• 正文
    • 直接傳遞棧中內(nèi)存地址
    • 多個客戶端同時連接服務器
    • 傳遞堆內(nèi)存地址
  • 相關(guān)推薦
申請入駐 產(chǎn)業(yè)圖譜

從0實現(xiàn)基于Linux socket聊天室-多線程服務器一個很隱晦的錯誤-2

01/26 10:24
578
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

根據(jù) 《0 基于socket和pthread實現(xiàn)多線程服務器模型》所述,server創(chuàng)建子線程的時候用的是以下代碼:

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一塊內(nèi)存專門存放這個新的套接字呢?

要講清楚這個問題的原因需要一些背景知識:

  1. Linux創(chuàng)建一個新進程時,新進程會創(chuàng)建一個主線程;
  2. 每個用戶進程有自己的地址空間,系統(tǒng)為每個用戶進程創(chuàng)建一個task_struct來描述該進程,實際上task_struct 和地址空間映射表一起用來,表示一個進程;
  3. Linux里同樣用task_struct來描述一個線程,線程和進程都參與統(tǒng)一的調(diào)度;
  4. 進程內(nèi)的不同線程執(zhí)行是同一程序的不同部分,各個線程并行執(zhí)行,受操作系統(tǒng)異步調(diào)度;
  5. 由于進程的地址空間是私有的,因此在進程間上下文切換時,系統(tǒng)開銷比較大;
  6. 在同一個進程中創(chuàng)建的線程共享該進程的地址空間。

明白這些基礎(chǔ)知識后,下面我來看下,當進程創(chuàng)建一個子線程的時候,傳遞的參數(shù)情況:

直接傳遞棧中內(nèi)存地址

我們首先分析下如果創(chuàng)建子線程傳遞的是局部變量new_fd的地址這種情況。

由上圖所示:

  1. 創(chuàng)建一個線程,如果我們按照圖中傳遞參數(shù)方法,那么new_fd是在棧中的,創(chuàng)建子線程的時候我們把new_fd地址傳遞給了thread1,線程回調(diào)參數(shù)arg的地址是new_fd地址。
  2. 因為主函數(shù)會一直循環(huán)不退出,所以new_fd一直存在棧中。用這種方法的確可以把new_fd的值3傳遞到子線程的局部變量fd,這樣子線程就可以使用這個fd與客戶端通信
  3. 但是因為我們設計的是并發(fā)服務器模型,我們沒有辦法預測客戶端什么時候會連接我們的服務器,假設遇到一個極端情況,在同一時刻,多個客戶端同時連接服務器,那么主線程是要同時創(chuàng)建多個子線程的。

多個客戶端同時連接服務器

如上圖所示,所有新建的的thread回調(diào)函數(shù)的參數(shù)arg存放的都是new_fd的地址。如果客戶端連接的時候時間間隔比較大,是沒有問題的,但是在一些極端的情況下還是有可能出現(xiàn)由于高并發(fā)引起的錯誤。

我們來捋一下極端的調(diào)用時序:

第一步:

如上圖所示:

  1. T1時刻,當客戶端1連接服務器的時候,服務器的accept函數(shù)會創(chuàng)建新的套接字4;
  2. T2時刻,創(chuàng)建了子線程thread1,同時子線程回調(diào)函數(shù)參數(shù)arg指向了棧中new_fd對應的內(nèi)存。
  3. 假設,正在此時,又有一個客戶端要連接服務器,而且thread1頁已經(jīng)用盡了時間片,那么主線程server會被調(diào)度到。

第二步:

如上圖所示:

  1. 5T3時刻,主線程server接受了客戶端的連接,accept函數(shù)會創(chuàng)建新的套接字5,同時創(chuàng)建子線程thread2,此時OS調(diào)度的thread2;
  2. T4時刻,thread2通過arg得到new_fd了的值5,并存入fd;
  3. T5時刻,時間片到了,調(diào)度thread1,thread1通過arg去讀取new_fd,此時棧中new_fd的值已經(jīng)修5覆蓋了;
  4. 所以出現(xiàn)了2個線程同時使用同一個fd的情況發(fā)生。

這種情況的發(fā)生,雖然概率很低,但是并不代表不發(fā)生,該bug就是一口君在解決實際項目中遇到過的。

傳遞堆內(nèi)存地址

如果采用傳遞堆的地址的方式,我們看下圖:

  1. T1時刻,當客戶端1連接服務器的時候,服務器的accept函數(shù)會創(chuàng)建新的套接字4,在堆中申請一塊內(nèi)存,用指針pconnsocke指向該內(nèi)存,同時將4保存到堆中;
  2. T2時刻,創(chuàng)建了子線程thread1,同時子線程回調(diào)函數(shù)參數(shù)arg指向了堆中pconnsocke指向的內(nèi)存。
  3. 假設,正在此時,又有一個客戶端要連接服務器,而且thread1頁已經(jīng)用盡了時間片,那么主線程server會被調(diào)度到。
  4. T3時刻,主線程server接受了客戶端的連接,accept函數(shù)會創(chuàng)建新的套接字5,在堆中申請一塊內(nèi)存,用指針pconnsocke指向該內(nèi)存,同時將5保存到堆中,然后創(chuàng)建子線程thread2;
  5. T4時刻,thread2通過arg指向了堆中pconnsocke指向的內(nèi)存,此處值為5,并存入fd;
  6. T5時刻,時間片到了,調(diào)度thread1,thread1通過arg去讀取fd,此時堆中數(shù)據(jù)位5;
  7. 就不會出現(xiàn)了2個線程同時使用同一個fd的情況發(fā)生。

這個知識點有點隱蔽,希望讀者在使用的時候多加小心。下一章,我們要講解如何利用我們現(xiàn)有的代碼實現(xiàn)登錄注冊的功能。

相關(guān)推薦

登錄即可解鎖
  • 海量技術(shù)文章
  • 設計資源下載
  • 產(chǎn)業(yè)鏈客戶資源
  • 寫文章/發(fā)需求
立即登錄

公眾號『一口Linux』號主彭老師,擁有15年嵌入式開發(fā)經(jīng)驗和培訓經(jīng)驗。曾任職ZTE,某研究所,華清遠見教學總監(jiān)。擁有多篇網(wǎng)絡協(xié)議相關(guān)專利和軟件著作。精通計算機網(wǎng)絡、Linux系統(tǒng)編程、ARM、Linux驅(qū)動、龍芯、物聯(lián)網(wǎng)。原創(chuàng)內(nèi)容基本從實際項目出發(fā),保持原理+實踐風格,適合Linux驅(qū)動新手入門和技術(shù)進階。