欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Linux中的幾種IO模型詳解

 更新時(shí)間:2025年08月05日 10:49:37   作者:??小陳在拼命??  
文章系統(tǒng)講解了幾種IO模型(阻塞、非阻塞、信號(hào)驅(qū)動(dòng)、多路轉(zhuǎn)接、異步),通過(guò)釣魚(yú)比喻幫助理解,重點(diǎn)分析了select函數(shù)的實(shí)現(xiàn)原理、就緒條件及缺點(diǎn),強(qiáng)調(diào)其通過(guò)位圖監(jiān)控多文件描述符狀態(tài),但存在上限限制和頻繁數(shù)據(jù)拷貝問(wèn)題

一、五種IO模型

1.1 高效IO的初步理解

IO其實(shí)就是“input”和“output”尤其在網(wǎng)絡(luò)部分,IO的特性非常明顯!

如果是在本地文件,本質(zhì)上就是將數(shù)據(jù)寫(xiě)到內(nèi)核文件緩沖區(qū),具體什么時(shí)候刷到磁盤(pán)上,是由OS決定的??!而在網(wǎng)絡(luò)中,本質(zhì)上也是將數(shù)據(jù)寫(xiě)到發(fā)送緩沖區(qū),但是具體什么時(shí)候發(fā)送,也是由OS決定的!!

所以應(yīng)用層進(jìn)行read或write的時(shí)候,本質(zhì)上是把數(shù)據(jù)從用戶層寫(xiě)給OS!這也是IO的本質(zhì)!read和write函數(shù)的本質(zhì)其實(shí)就是拷貝函數(shù)!

但是拷貝并不是一定能立馬執(zhí)行的!比如說(shuō)read的時(shí)候,如果我的接收緩沖區(qū)沒(méi)有數(shù)據(jù),我得阻塞,而write的時(shí)候,我的發(fā)送緩沖區(qū)滿了,那么我也得阻塞!!

所以要進(jìn)行拷貝!必須要先判斷讀寫(xiě)事件是否就緒!!

IO=等+拷貝

問(wèn)題1:什么是讀寫(xiě)事件呢??

——>你想讀就得等讀事件就緒,就是接收緩沖區(qū)有數(shù)據(jù),想寫(xiě)就得等寫(xiě)事件就緒,就是等發(fā)送緩沖區(qū)要有足夠多的空間,想讀就是得讀事件就緒,以上統(tǒng)稱讀寫(xiě)事件就緒!

問(wèn)題2:什么是高效的IO呢??

——>任何IO過(guò)程中, 都包含兩個(gè)步驟. 第一是等待, 第二是拷貝. 而且在實(shí)際的應(yīng)用場(chǎng)景中, 等待消耗的時(shí)間往 往都遠(yuǎn)遠(yuǎn)高于拷貝的時(shí)間. 讓IO更高效, 最核心的辦法就是單位時(shí)間內(nèi)等待時(shí)間的比重減少!

問(wèn)題3:怎么理解等的比重減少呢?

——>比如說(shuō)你當(dāng)前是單進(jìn)程,如果讀寫(xiě)時(shí)間沒(méi)有就緒就會(huì)阻塞住,只會(huì)等一個(gè)文件描述符,而如果是多線程,他可以等待多個(gè)文件描述符,此時(shí)的IO等待時(shí)間不是串型的而是并行的!!

1.2 用“釣魚(yú)”理解五種IO模型

接下來(lái)我們就要介紹五種IO模型,什么叫模型呢??其實(shí)就是規(guī)律,未來(lái)不管是讀文件還是寫(xiě)文件都離不開(kāi)其中一種!

釣魚(yú)=等+釣(可以比喻IO)

1、張三(新手) 拿著自己的魚(yú)漂(用來(lái)主動(dòng)檢測(cè)讀寫(xiě)事件是否就緒) 魚(yú)竿(相當(dāng)于文件描述符) 魚(yú)鉤坐在椅子上,然后一下鉤就死死盯著魚(yú)漂, 魚(yú)漂不動(dòng)張三也不動(dòng),誰(shuí)找他喊他他都不回應(yīng) 直到魚(yú)上鉤 ----這是阻塞式IO(策略是在內(nèi)核將數(shù)據(jù)準(zhǔn)備好之前, 系統(tǒng)調(diào)用會(huì)一直等待所有的套接字, 默認(rèn)都是阻塞方式.)

2、李四(有兩三年釣魚(yú)經(jīng)驗(yàn),坐不?。┖皬埲?,張三不理他 他也就坐在那釣魚(yú)了 但是他比較坐不住,他會(huì)每隔一段時(shí)間檢查一下魚(yú)漂,不會(huì)一直死死盯著,其他時(shí)間他會(huì)把視線轉(zhuǎn)移到自己的手機(jī)上刷抖音,所以他檢測(cè)的時(shí)候如果檢測(cè)不到就會(huì)立刻做自己的事情 不會(huì)一直死盯 檢測(cè)條件就緒了才釣魚(yú) ——這是非阻塞等待IO(策略是如果內(nèi)核還未將數(shù)據(jù)準(zhǔn)備好, 系統(tǒng)調(diào)用仍然會(huì)直接返回, 并且返回EWOULDBLOCK錯(cuò)誤碼.)

非阻塞IO往往需要程序員循環(huán)的方式反復(fù)嘗試讀寫(xiě)文件描述符, 這個(gè)過(guò)程稱為輪詢. 這對(duì)CPU來(lái)說(shuō)是較大的浪費(fèi), 一 般只有特定場(chǎng)景下才使用.

3、王五 (有五年釣魚(yú)經(jīng)驗(yàn)) 他看張三和李四一個(gè)一直動(dòng),一個(gè)一動(dòng)不動(dòng),覺(jué)得他們是菜鳥(niǎo),他也跟著釣魚(yú)了,然后他在魚(yú)竿上綁了一個(gè)鈴鐺 然后他就把魚(yú)竿插起來(lái)不管了 直接躺在旁邊玩手機(jī) 基本不關(guān)注魚(yú)竿,直接等鈴鐺響 他才會(huì)去把魚(yú)釣上來(lái)。 我們會(huì)發(fā)現(xiàn)張三和李四是主動(dòng)去檢測(cè)的 而王五的方式就是我不會(huì)主動(dòng)檢測(cè),就是魚(yú)上鉤了會(huì)自己通知我 ——信號(hào)驅(qū)動(dòng)式IO(策略是內(nèi)核將數(shù)據(jù)準(zhǔn)備好的時(shí)候, 使用SIGIO信號(hào)通知應(yīng)用程序進(jìn)行IO操作. )

4、趙六(富豪、好勝) 所以他拉了一卡車(chē)的魚(yú)竿 把所有的魚(yú)竿都插起來(lái) 然后他會(huì)來(lái)回走動(dòng)檢測(cè)(周期性遍歷)哪邊有魚(yú)上鉤 ——這就是多路轉(zhuǎn)接(策略最核心在于IO多路轉(zhuǎn)接能夠同時(shí)等待多個(gè)文件描述符的就緒狀態(tài))

5、田七(世界首富 但是不是很專業(yè)) 司機(jī)開(kāi)車(chē)帶著他經(jīng)過(guò)河邊的時(shí)候,他發(fā)現(xiàn)河邊有4個(gè)非常奇怪的人 釣魚(yú)的姿勢(shì)形態(tài)各異 于是他就很好奇 也想去釣魚(yú) 然后突然公司打電話要開(kāi)緊急會(huì)議 可是他又想吃魚(yú) 于是他就把司機(jī)小王叫了過(guò)來(lái) 說(shuō)我要去開(kāi)會(huì) 你幫我釣魚(yú) 等你釣滿一桶了打電話給我 我再讓人來(lái)接你

田七并不是喜歡釣魚(yú) 他是釣魚(yú)行為的發(fā)起者 他要的是魚(yú)(數(shù)據(jù)) 田七這種方式叫做——異步IO (由內(nèi)核在數(shù)據(jù)拷貝完成時(shí), 通知應(yīng)用程序)

因?yàn)樾⊥踉卺烎~(yú)的時(shí)候 他正在開(kāi)會(huì) 此時(shí)的小王就相當(dāng)于是OS 桶就相當(dāng)于是一段緩沖區(qū),電話就相當(dāng)于是一種通知方式 他將IO工作交給了OS 由OS自動(dòng)去檢測(cè)然后將數(shù)據(jù)放在緩沖區(qū)里 等緩沖區(qū)滿了就通知你來(lái)取 田七在應(yīng)用層用就可以了,田七并不參與具體的IO過(guò)程 而前四種方式就叫做同步IO

問(wèn)題1:為什么趙六效率最高呢??拿到魚(yú)竿多效率及高么??

——>假設(shè)你是一條魚(yú) 你看到旁邊這么多魚(yú)竿 你會(huì)咬哪一個(gè)呢??顯然趙六釣到魚(yú)的機(jī)會(huì)最大,因?yàn)槎鄠€(gè)魚(yú)竿可以讓我們每一個(gè)等待的過(guò)程在時(shí)間上是并行重疊的??!所以整體上等的比重就減少了!!

問(wèn)題2:阻塞IOvs非阻塞IO

——>阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時(shí)的狀態(tài).

阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會(huì)被掛起. 調(diào)用線程只有在得到結(jié)果之后才會(huì)返回.

非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會(huì)阻塞當(dāng)前線程.

在效率方面沒(méi)有任何區(qū)別(因?yàn)镮O=等+拷貝 大家的區(qū)別只是等的方式不同),我們一般說(shuō)非阻塞效率會(huì)高一點(diǎn)不是IO效率高 而是他在非阻塞輪詢的時(shí)候可以做其他的事情

問(wèn)題3:王五有等嗎??

——>王五也算一種等?。∫蝗凰麨槭裁床恢苯踊丶夷???就算我們說(shuō)他沒(méi)等,魚(yú)咬鉤的時(shí)候他也要參與釣魚(yú)的過(guò)程(IO) 只要有參與IO,就一定有同步的過(guò)程,所以也是同步IO

問(wèn)題4:同步IOVS 異步IO

——>同步IO就是有參與O的過(guò)程,而異步IO就只是發(fā)起IO,但是并不參與IO的過(guò)程,OS完成IO后會(huì)通知上層拿結(jié)果,然后直接用就行了!!

問(wèn)題5:同步通信vs異步通信

——>同步和異步關(guān)注的是消息通信機(jī)制.

所謂同步,就是在發(fā)出一個(gè)調(diào)用時(shí),在沒(méi)有得到結(jié)果之前,該調(diào)用就不返回. 但是一旦調(diào)用返回,就得到返回值了; 換句話說(shuō),就是由調(diào)用者主動(dòng)等待這個(gè)調(diào)用的結(jié)果;

異步則是相反,調(diào)用在發(fā)出之后,這個(gè)調(diào)用就直接返回了,所以沒(méi)有返回結(jié)果; 換句話說(shuō),當(dāng)一個(gè)異步過(guò)程調(diào)用發(fā)出后,調(diào)用者不會(huì)立刻得到結(jié)果; 而是在調(diào)用發(fā)出后,被調(diào)用者通過(guò)狀態(tài)、通知來(lái)通知調(diào)用者,或通過(guò)回調(diào)函數(shù)處理這個(gè)調(diào)用.

問(wèn)題6:同步IOVS 線程同步

——>他倆就是老婆和老婆餅的關(guān)系(毫無(wú)關(guān)聯(lián)?。絀O是IO層面的概念,而線程同步是兩個(gè)線程誰(shuí)先誰(shuí)后的問(wèn)題!!所以以后在看到 "同步" 這個(gè)詞, 一定要先搞清楚大背景是什么. 這個(gè)同步, 是同步通信異步通信的同步, 還是同步 與互斥的同步.

問(wèn)題7:異步IO效率不高呢??為什么實(shí)際場(chǎng)景多路轉(zhuǎn)接用的多?

——>田七再厲害也只有一套裝備 而且異步IO寫(xiě)出來(lái)的服務(wù)邏輯比較混亂 所以現(xiàn)在已經(jīng)有很多方法(比如協(xié)程)在逐步取代異步IO了 所以這里最值得我們學(xué)習(xí)的是多路轉(zhuǎn)接和非阻塞!!

問(wèn)題8:異步IOvs信號(hào)驅(qū)動(dòng)

——>異步IO是由OS完成拷貝的過(guò)程然后通知上層,而信號(hào)驅(qū)動(dòng)是告訴上層可以進(jìn)行拷貝了

問(wèn)題9:其他高級(jí)IO

——>非阻塞IO,紀(jì)錄鎖,系統(tǒng)V流機(jī)制,I/O多路轉(zhuǎn)接(也叫I/O多路復(fù)用),readv和writev函數(shù)以及存儲(chǔ)映射IO(mmap),這些統(tǒng)稱為高級(jí)IO.

二、非阻塞輪詢

我們會(huì)發(fā)現(xiàn)以上接口有一個(gè)flag參數(shù),我們可以通過(guò)設(shè)置來(lái)讓該事件以非阻塞輪詢的方式來(lái)訪問(wèn)套接字,但是這種方法太麻煩了!!

因?yàn)槲覀冏x寫(xiě)本質(zhì)就是讀寫(xiě)文件描述符指向的文件緩沖區(qū),而文件描述符本質(zhì)上是下標(biāo),所以更通用的做法就是把文件描述符屬性設(shè)置成非阻塞(其實(shí)就是他指向的文件對(duì)象struct file里面的一個(gè)標(biāo)志位)告訴內(nèi)核這個(gè)文件描述符我們要以非阻塞的方式來(lái)操作!

2.1fcntl

一個(gè)文件描述符, 默認(rèn)都是阻塞IO.

int fcntl(int fd, int cmd, ... /* arg */ );

傳入的cmd的值不同, 后面追加的參數(shù)也不相同. fcntl函數(shù)有5種功能:

  • 復(fù)制一個(gè)現(xiàn)有的描述符(cmd=F_DUPFD).
  • 獲得/設(shè)置文件描述符標(biāo)記(cmd=F_GETFD或F_SETFD).
  • 獲得/設(shè)置文件狀態(tài)標(biāo)記(cmd=F_GETFL或F_SETFL).
  • 獲得/設(shè)置異步I/O所有權(quán)(cmd=F_GETOWN或F_SETOWN).
  • 獲得/設(shè)置記錄鎖(cmd=F_GETLK,F_SETLK或F_SETLKW).

我們此處只是用第三種功能, 獲取/設(shè)置文件狀態(tài)標(biāo)記, 就可以將一個(gè)文件描述符設(shè)置為非阻塞.

2.2 實(shí)現(xiàn)函數(shù)SetNoBlock

基于fcntl, 我們實(shí)現(xiàn)一個(gè)SetNoBlock函數(shù), 將文件描述符設(shè)置為非阻塞.

void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
    cout << " set " << fd << " nonblock done" << endl;
}

使用F_GETFL將當(dāng)前的文件描述符的屬性取出來(lái)(這是一個(gè)位圖).

然后再使用F_SETFL將文件描述符設(shè)置回去. 設(shè)置回去的同時(shí), 加上一個(gè)O_NONBLOCK參數(shù).

2.3 輪詢方式讀取標(biāo)準(zhǔn)輸入

int main()
{
    char buffer[1024];
    SetNonBlock(0);
    sleep(1);
    while (true)
    {
        // printf("Please Enter# ");
        // fflush(stdout);

        ssize_t n = read(0, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n - 1] = 0;
            cout << "echo : " << buffer << endl;
        }
        else if (n == 0)
        {
            cout << "read done" << endl;
            break;
        }
        else
        {
            // 1. 設(shè)置成為非阻塞,如果底層fd數(shù)據(jù)沒(méi)有就緒,recv/read/write/send, 返回值會(huì)以出錯(cuò)的形式返回
            // 2. a. 真的出錯(cuò) b. 底層沒(méi)有就緒
            // 3. 我怎么區(qū)分呢?通過(guò)errno區(qū)分!?。?
            if (errno == EWOULDBLOCK)
            {
                cout << "0 fd data not ready, try again!" << endl;
                // do_other_thing();
                sleep(1);
            }
            else
            {
                cerr << "read error, n = " << n << "errno code: "
                     << errno << ", error str: " << strerror(errno) << endl;
            }
            // TODO 信號(hào)中斷IO?
        }
    }

    return 0;
}

問(wèn)題:如果將文件描述符設(shè)置為非阻塞了,如果底層fd數(shù)據(jù)沒(méi)有就緒,recv/read/write/send,返回值會(huì)以出錯(cuò)(-1)的返回,為什么呢??

——>因?yàn)樗麑?shí)在沒(méi)辦法了!!>0表示成功,=0表示關(guān)閉,那么只能是<0了

所以此時(shí)<0有兩種情況(1)真的出錯(cuò)了 (2)底層讀寫(xiě)事件沒(méi)有就緒

那我怎么區(qū)分呢??所以規(guī)定在返回-1的時(shí)候會(huì)設(shè)置錯(cuò)誤碼,我們可以通過(guò)錯(cuò)誤碼去判斷!

因此一旦被設(shè)置為非阻塞了,那么返回-1情況在分類(lèi)討論的時(shí)候還需要根據(jù)錯(cuò)誤碼加一層判斷,不能直接break

當(dāng)然我們也可以寫(xiě)一個(gè)函數(shù)讓他在輪詢的期間去做點(diǎn)別的事情!

三、select-多路轉(zhuǎn)接

以前我們學(xué)到的大多數(shù)接口是既等又IO,而現(xiàn)在我們可以用一個(gè)select專門(mén)用來(lái)等,并且他一次可以等待多個(gè)文件描述符,從而在等的時(shí)間上實(shí)現(xiàn)并行??!

3.1 select介紹

系統(tǒng)提供select函數(shù)來(lái)實(shí)現(xiàn)多路復(fù)用輸入/輸出模型

select系統(tǒng)調(diào)用是用來(lái)讓我們的程序監(jiān)視多個(gè)文件描述符的狀態(tài)變化的;

程序會(huì)停在select這里等待,直到被監(jiān)視的文件描述符有一個(gè)或多個(gè)發(fā)生了狀態(tài)改變;

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 

3.1.1 參數(shù)解釋

  • nfds:是需要監(jiān)視的最大的文件描述符值+1;

因?yàn)槲募枋龇窍聵?biāo),所以可以理解為監(jiān)聽(tīng)的文件描述符的范圍。

  • rdset,wrset,exset:分別對(duì)應(yīng)于需要檢測(cè)的 可讀文件描述符的集合,可寫(xiě)文件描述符的集合及異常文件描述符的集合;

輸入輸出型參數(shù)!設(shè)置時(shí)表示要監(jiān)聽(tīng)的文件描述符,返回時(shí)由內(nèi)核設(shè)置,表示已經(jīng)就緒的文件描述符

  • timeout:結(jié)構(gòu)timeval,用來(lái)設(shè)置select()的等待時(shí)間

輸入輸出型參數(shù)! 比如等待時(shí)間是5s,如果2秒就有文件描述符就緒了,那么就會(huì)返回3秒

3.1.2 關(guān)于timeval結(jié)構(gòu)體

timeval結(jié)構(gòu)用于描述一段時(shí)間長(zhǎng)度,如果在這個(gè)時(shí)間內(nèi),需要監(jiān)視的描述符沒(méi)有事件發(fā)生則函數(shù)返回,返回值為0。

關(guān)于取值:

  • NULL:則表示select()沒(méi)有timeout,select將一直被阻塞,直到某個(gè)文件描述符上發(fā)生了事件;
  • 0:僅檢測(cè)描述符集合的狀態(tài),然后立即返回,并不等待外部事件的發(fā)生。
  • 特定的時(shí)間值:如果在指定的時(shí)間段里沒(méi)有事件發(fā)生,select將超時(shí)返回。(第一個(gè)為單位s,第二個(gè)單位為ms)

3.1.3 關(guān)于fd_set結(jié)構(gòu)體

這個(gè)結(jié)構(gòu)是由內(nèi)核提供的一種數(shù)據(jù)類(lèi)型,其實(shí)就是一個(gè)整數(shù)數(shù)組, 更嚴(yán)格的說(shuō), 是一個(gè) "位圖". 使用位圖中對(duì)應(yīng)的位來(lái)表示要監(jiān)視的文件描述符. (用來(lái)給用戶和內(nèi)核做溝通)

他是一個(gè)輸入輸出型參數(shù)??!

  • 輸入時(shí),由用戶告訴內(nèi)核:我給你的一個(gè)或者多個(gè)fd,你要幫我關(guān)心上面的事件哦!如果就緒了你一定要告訴我哈??!
  • 輸出時(shí),由內(nèi)核告訴用戶:你讓我關(guān)心的多個(gè)fd中,有一些已經(jīng)就緒了哦,用戶你趕緊讀取吧

所以使用select注定一定有大量位圖操作!

用戶:這個(gè)位圖由我自己來(lái)操作嗎??

OS說(shuō):你還是別直接操作了吧,你連他的結(jié)構(gòu)都沒(méi)搞清楚,還是讓我來(lái)給你提供一批操作位圖的接口吧??!所以提供了一組操作fd_set的接口, 來(lái)比較方便的操作位圖,

void FD_CLR(int fd, fd_set *set); // 用來(lái)清除描述詞組set中相關(guān)fd的位
int FD_ISSET(int fd, fd_set *set); // 用來(lái)測(cè)試描述詞組set中相關(guān)fd的位是否為真
void FD_SET(int fd, fd_set *set); // 用來(lái)設(shè)置描述詞組set中相關(guān)fd的位
void FD_ZERO(fd_set *set); // 用來(lái)清除描述詞組set的全部位

3.1.4 函數(shù)返回值

執(zhí)行成功則返回文件描述詞狀態(tài)已改變的個(gè)數(shù)

如果返回0代表在描述詞狀態(tài)改變前已超過(guò)timeout時(shí)間,沒(méi)有返回

當(dāng)有錯(cuò)誤發(fā)生時(shí)則返回-1,錯(cuò)誤原因存于errno,此時(shí)參數(shù)readfds,writefds, exceptfds和timeout的 值變成不可預(yù)測(cè)

錯(cuò)誤值可能為:

  • EBADF 文件描述詞為無(wú)效的或該文件已關(guān)閉
  • EINTR 此調(diào)用被信號(hào)所中斷
  • EINVAL 參數(shù)n 為負(fù)值。
  • ENOMEM 核心內(nèi)存不足

3.2 理解select執(zhí)行過(guò)程

理解select模型的關(guān)鍵在于理解fd_set,為說(shuō)明方便,取fd_set長(zhǎng)度為1字節(jié),fd_set中的每一bit可以對(duì)應(yīng)一個(gè)文件描述符fd。則1字節(jié)長(zhǎng)的fd_set最大可以對(duì)應(yīng)8個(gè)fd.

(1)執(zhí)行fd_set set; FD_ZERO(&set);則set用位表示是0000,0000。

(2)若fd=5,執(zhí)行FD_SET(fd,&set);

后set變?yōu)?001,0000(第5位置為1)

(3)若再加入fd=2,fd=1,則set變?yōu)?001,0011

(4)執(zhí)行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都發(fā)生可讀事件,則select返回,此時(shí)set變?yōu)?000,0011。注意:沒(méi)有事件發(fā)生的fd=5被清空。

3.3socket就緒條件

讀就緒

  • socket內(nèi)核中, 接收緩沖區(qū)中的字節(jié)數(shù), 大于等于低水位標(biāo)記SO_RCVLOWAT. 此時(shí)可以無(wú)阻塞的讀該文件描述符, 并且返回值大于0;
  • socket TCP通信中, 對(duì)端關(guān)閉連接, 此時(shí)對(duì)該socket讀, 則返回0;
  • 監(jiān)聽(tīng)的socket上有新的連接請(qǐng)求;
  • socket上有未處理的錯(cuò)誤;

寫(xiě)就緒

  • socket內(nèi)核中, 發(fā)送緩沖區(qū)中的可用字節(jié)數(shù)(發(fā)送緩沖區(qū)的空閑位置大小), 大于等于低水位標(biāo)記SO_SNDLOWAT, 此時(shí)可以無(wú)阻塞的寫(xiě), 并且返回值大于0;
  • socket的寫(xiě)操作被關(guān)閉(close或者shutdown). 對(duì)一個(gè)寫(xiě)操作被關(guān)閉的socket進(jìn)行寫(xiě)操作, 會(huì)觸發(fā)SIGPIPE信號(hào);
  • socket使用非阻塞connect連接成功或失敗之后;
  • socket上有未讀取的錯(cuò)誤;

異常就緒

socket上收到帶外數(shù)據(jù). 關(guān)于帶外數(shù)據(jù), 和TCP緊急模式相關(guān)(回憶TCP協(xié)議頭中, 有一個(gè)緊急指針的字段),

3.4 通過(guò)編碼深入理解

Socket.hpp

#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

enum
{
    SocketErr = 2,
    BindErr,
    ListenErr,
};

// TODO
const int backlog = 10;

class Sock
{
public:
    Sock()
    {
    }
    ~Sock()
    {
    }

public:
    void Socket()
    {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0)
        {
            lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
            exit(SocketErr);
        }
        int opt = 1;
        setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
    }
    void Bind(uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
            exit(BindErr);
        }
    }
    void Listen()
    {
        if (listen(sockfd_, backlog) < 0)
        {
            lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
            exit(ListenErr);
        }
    }
    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
        if(newfd < 0)
        {
            lg(Warning, "accept error, %s: %d", strerror(errno), errno);
            return -1;
        }
        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);

        return newfd;
    }
    bool Connect(const std::string &ip, const uint16_t &port)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));

        int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
        if(n == -1) 
        {
            std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
            return false;
        }
        return true;
    }
    void Close()
    {
        close(sockfd_);
    }
    int Fd()
    {
        return sockfd_;
    }

private:
    int sockfd_;
};

log.hpp

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    // void logmessage(int level, const char *format, ...)
    // {
    //     time_t t = time(nullptr);
    //     struct tm *ctime = localtime(&t);
    //     char leftbuffer[SIZE];
    //     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    //              ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
    //              ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

    //     // va_list s;
    //     // va_start(s, format);
    //     char rightbuffer[SIZE];
    //     vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
    //     // va_end(s);

    //     // 格式:默認(rèn)部分+自定義部分
    //     char logtxt[SIZE * 2];
    //     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

    //     // printf("%s", logtxt); // 暫時(shí)打印
    //     printLog(level, logtxt);
    // }
    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt);
    }

    ~Log()
    {
    }
    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 格式:默認(rèn)部分+自定義部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 暫時(shí)打印
        printLog(level, logtxt);
    }

private:
    int printMethod;
    std::string path;
};

Log lg;

// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);

//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
//         n--;
//     }

//     va_end(s); //s = NULL
//     return sum;
// }

Makefile:

select_server:Main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f select_server

SelectServer.hpp:

#pragma once

#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"

using namespace std;

static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;

class SelectServer
{
public:
    SelectServer(uint16_t port = defaultport) : _port(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            fd_array[i] = defaultfd;
            // std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
        }
    }
    bool Init()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();

        return true;
    }
    void Accepter()
    {
        // 我們的連接事件就緒了
        std::string clientip;
        uint16_t clientport = 0;
        int sock = _listensock.Accept(&clientip, &clientport); // 會(huì)不會(huì)阻塞在這里?不會(huì)
        if (sock < 0) return;
        lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);

        // sock -> fd_array[]
        int pos = 1;
        for (; pos < fd_num_max; pos++) // 第二個(gè)循環(huán)
        {
            if (fd_array[pos] != defaultfd)
                continue;
            else
                break;
        }
        if (pos == fd_num_max)
        {
            lg(Warning, "server is full, close %d now!", sock);
            close(sock);
        }
        else
        {
            fd_array[pos] = sock;
            PrintFd();
            // TODO
        }
    }
    void Recver(int fd, int pos)
    {
        // demo
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
        if (n > 0)
        {
            buffer[n] = 0;
            cout << "get a messge: " << buffer << endl;
        }
        else if (n == 0)
        {
            lg(Info, "client quit, me too, close fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultfd; // 這里本質(zhì)是從select中移除
        }
        else
        {
            lg(Warning, "recv error: fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultfd; // 這里本質(zhì)是從select中移除
        }
    }
    void Dispatcher(fd_set &rfds)
    {
        for (int i = 0; i < fd_num_max; i++) // 這是第三個(gè)循環(huán)
        {
            int fd = fd_array[i];
            if (fd == defaultfd)
                continue;

            if (FD_ISSET(fd, &rfds))
            {
                if (fd == _listensock.Fd())
                {
                    Accepter(); // 連接管理器
                }
                else // non listenfd
                {
                    Recver(fd, i);
                }
            }
        }
    }
    void Start()
    {
        int listensock = _listensock.Fd();
        fd_array[0] = listensock;
        for (;;)
        {
            fd_set rfds;
            FD_ZERO(&rfds);

            int maxfd = fd_array[0];
            for (int i = 0; i < fd_num_max; i++) // 第一次循環(huán)
            {
                if (fd_array[i] == defaultfd)
                    continue;
                FD_SET(fd_array[i], &rfds);
                if (maxfd < fd_array[i])
                {
                    maxfd = fd_array[i];
                    lg(Info, "max fd update, max fd is: %d", maxfd);
                }
            }

            // accept?不能直接accept!檢測(cè)并獲取listensock上面的事件,新連接到來(lái),等價(jià)于讀事件就緒

            // struct timeval timeout = {1, 0}; // 輸入輸出,可能要進(jìn)行周期的重復(fù)設(shè)置
            struct timeval timeout = {0, 0}; // 輸入輸出,可能要進(jìn)行周期的重復(fù)設(shè)置
            // 如果事件就緒,上層不處理,select會(huì)一直通知你!
            // select告訴你就緒了,接下來(lái)的一次讀取,我們讀取fd的時(shí)候,不會(huì)被阻塞
            // rfds: 輸入輸出型參數(shù)。 1111 1111 -> 0000 0000
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
            switch (n)
            {
            case 0:
                cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
                break;
            case -1:
                cerr << "select error" << endl;
                break;
            default:
                // 有事件就緒了,TODO
                cout << "get a new link!!!!!" << endl;
                Dispatcher(rfds); // 就緒的事件和fd你怎么知道只有一個(gè)呢???
                break;
            }
        }
    }
    void PrintFd()
    {
        cout << "online fd list: ";
        for (int i = 0; i < fd_num_max; i++)
        {
            if (fd_array[i] == defaultfd)
                continue;
            cout << fd_array[i] << " ";
        }
        cout << endl;
    }
    ~SelectServer()
    {
        _listensock.Close();
    }

private:
    Sock _listensock;
    uint16_t _port;
    int fd_array[fd_num_max];   // 數(shù)組, 用戶維護(hù)的!
    // int wfd_array[fd_num_max];
};

Main.cc

#include "SelectServer.hpp"
#include <memory>

int main()
{
    // std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl;

    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->Init();
    svr->Start();

    return 0;
}

注意事項(xiàng):

1、不能直接aceept,因?yàn)樗蟛糠謺r(shí)間都在等,一次只能等一個(gè)文件描述符?。。╨istensock上面的時(shí)間是新鏈接到來(lái),就是三次握手完成,鏈接投遞到全連接隊(duì)列里,然后你再通過(guò)accept把鏈接從底層拿上來(lái)),所以新鏈接來(lái)了相當(dāng)于是讀事件就緒!!

2、 定義fd_set類(lèi)型變量如果是在棧上定義,可能會(huì)出現(xiàn)亂碼,所以在使用前要記得先清空!!

3、因?yàn)閠imeout是輸入輸出型參數(shù)!!所以返回之后可能已經(jīng)修改過(guò)了??!所以為了維持他的效果我們就必須周期性重復(fù)設(shè)置!

4、因?yàn)椋?)rfds是一個(gè)輸入輸出型參數(shù),每次都會(huì)被重新設(shè)置,且隨著不斷獲取新鏈接,套接字的數(shù)量會(huì)越來(lái)越多!不能寫(xiě)死,應(yīng)是動(dòng)態(tài)計(jì)算 (2)select不僅僅要等lisentsock,也要等讀的sock

因此需要有一個(gè)輔助數(shù)組arrry來(lái)監(jiān)控select中的fd,他不僅可以方便我們

(1)將文件描述符信息在不同函數(shù)之間的傳遞

(2)用于在select 返回后,array作為源數(shù)據(jù)和fd_set進(jìn)行FD_ISSET判斷。。

(3)select返回后會(huì)把以前加入的但并無(wú)事件發(fā)生的fd清空,則每次開(kāi)始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時(shí)取得fd最大值maxfd,用于select的第一個(gè)參數(shù)

(4)讓左側(cè)是監(jiān)聽(tīng)套接字,右側(cè)是讀套接字

5、輔助數(shù)組里有鏈接就緒和讀就緒,我們?cè)趺磪^(qū)分呢??——>確認(rèn)就緒之后,再加一層判斷。證明自己是否是監(jiān)聽(tīng)套接字。

6、關(guān)于Dispatcher(事件派發(fā)器),就是收到了多個(gè)就緒的文件描述符,然后跟array進(jìn)行判斷并派發(fā),如果是連接就緒就交給連接事件處理,如果是讀就緒就交給讀事件處理。

因?yàn)榫途w的時(shí)間不一定只有一個(gè),所以必須要循環(huán)去遍歷!

7、關(guān)于recver,讀的時(shí)候不能直接讀,因?yàn)樽x的時(shí)候內(nèi)容可能不完整,這就涉及到了協(xié)議的內(nèi)容!

3.5select缺點(diǎn)

1、等待的fd是有上限的!!

可監(jiān)控的文件描述符個(gè)數(shù)取決與sizeof(fd_set)的值. 我這邊服務(wù)器上sizeof(fd_set)=512,每bit表示一個(gè)文件 描述符,則我服務(wù)器上支持的最大文件描述符是512*8=4096.

備注: fd_set的大小可以調(diào)整,可能涉及到重新編譯內(nèi)核

2、輸入輸出型參數(shù)比較多,數(shù)據(jù)拷貝的頻率很高,且每次都需要對(duì)關(guān)心的fd進(jìn)行重置

3、用戶層是,使用第三方數(shù)組管理用戶的fd,用戶層需要多次遍歷,內(nèi)核中檢測(cè)fd時(shí)間就緒也要遍歷。

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Linux deepin 刪除多余內(nèi)核的實(shí)現(xiàn)方法

    Linux deepin 刪除多余內(nèi)核的實(shí)現(xiàn)方法

    這篇文章主要介紹了Linux deepin 刪除多余內(nèi)核的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-12-12
  • Linux中RPM文件操作的常用命令總結(jié)

    Linux中RPM文件操作的常用命令總結(jié)

    這篇文章主要給大家介紹了關(guān)于Linux中RPM文件操作的常用命令,文中通過(guò)示例介紹的很詳細(xì),對(duì)大家的理解和學(xué)習(xí)很有幫助,有需要的朋友們可以參考借鑒,下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。
    2016-11-11
  • linux CentOS 系統(tǒng)php和mysql命令加入到環(huán)境變量中

    linux CentOS 系統(tǒng)php和mysql命令加入到環(huán)境變量中

    這篇文章主要介紹了linux CentOS 系統(tǒng)php和mysql命令加入到環(huán)境變量中的相關(guān)資料,需要的朋友可以參考下
    2016-12-12
  • logrotate實(shí)現(xiàn)日志切割方式(轉(zhuǎn)儲(chǔ))

    logrotate實(shí)現(xiàn)日志切割方式(轉(zhuǎn)儲(chǔ))

    這篇文章主要介紹了logrotate實(shí)現(xiàn)日志切割方式(轉(zhuǎn)儲(chǔ)),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-05-05
  • linux中通過(guò)文件描述符獲取文件絕對(duì)路徑的方法

    linux中通過(guò)文件描述符獲取文件絕對(duì)路徑的方法

    下面小編就為大家?guī)?lái)一篇linux中通過(guò)文件描述符獲取文件絕對(duì)路徑的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2016-12-12
  • ubuntu系統(tǒng)下apache配置虛擬主機(jī)及反向代理詳解

    ubuntu系統(tǒng)下apache配置虛擬主機(jī)及反向代理詳解

    這篇文章主要介紹了ubuntu系統(tǒng)下apache配置虛擬主機(jī)及反向代理的相關(guān)資料,文中通過(guò)實(shí)例給大家演示的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-06-06
  • Linux系統(tǒng)中獲取時(shí)間的方法總結(jié)

    Linux系統(tǒng)中獲取時(shí)間的方法總結(jié)

    在Linux操作系統(tǒng)中,獲取時(shí)間是一個(gè)基本且重要的功能,本文旨在全面總結(jié)Linux系統(tǒng)中獲取時(shí)間的方法,包括命令行工具和編程接口,幫助讀者深入理解Linux時(shí)間管理的機(jī)制,需要的朋友可以參考下
    2025-03-03
  • 詳解Supervisor安裝與配置(Linux/Unix進(jìn)程管理工具)

    詳解Supervisor安裝與配置(Linux/Unix進(jìn)程管理工具)

    這篇文章主要介紹了詳解Supervisor安裝與配置(Linux/Unix進(jìn)程管理工具),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-06-06
  • 確保Linux服務(wù)器安全 防范四種級(jí)別攻擊

    確保Linux服務(wù)器安全 防范四種級(jí)別攻擊

    以下的文章主要描述的是防范四種級(jí)別攻擊確保Linux服務(wù)器安全,如果你對(duì)防范四種級(jí)別攻擊確保Linux服務(wù)器安全心存好奇的話,以下的文章將會(huì)揭開(kāi)它的神秘面紗。
    2011-03-03
  • linux系統(tǒng)報(bào)tcp_mark_head_lost錯(cuò)誤的處理方法

    linux系統(tǒng)報(bào)tcp_mark_head_lost錯(cuò)誤的處理方法

    這篇文章主要給大家介紹了關(guān)于linux系統(tǒng)報(bào)tcp_mark_head_lost錯(cuò)誤的處理方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用linux系統(tǒng)具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-07-07

最新評(píng)論