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

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

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

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

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)存專門(mén)存放這個(gè)新的套接字呢?

要講清楚這個(gè)問(wèn)題的原因需要一些背景知識(shí):

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

明白這些基礎(chǔ)知識(shí)后,下面我來(lái)看下,當(dāng)進(jìn)程創(chuàng)建一個(gè)子線程的時(shí)候,傳遞的參數(shù)情況:

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

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

由上圖所示:

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

多個(gè)客戶端同時(shí)連接服務(wù)器

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

我們來(lái)捋一下極端的調(diào)用時(shí)序:

第一步:

如上圖所示:

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

第二步:

如上圖所示:

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

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

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

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

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

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

相關(guān)推薦

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

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