識(shí)與Select)
bit::Shadow?(? ? ??目錄IO認(rèn)識(shí)同步 VS 異步阻塞 VS非阻塞fcntl常用cmdF_GETFL與F_GETFD舉例read返回值-1Select原型timeoutfd_set結(jié)構(gòu)Select通信原理0.怎么解決單進(jìn)程下復(fù)原問(wèn)題三狀態(tài)參數(shù)1.怎么區(qū)分新鏈接與信息Echo ServerFD_ISSET第一個(gè)參數(shù)fd正確判斷IO認(rèn)識(shí)IO分作等待 拷貝先前直接使用的recv / read / readfrom / get / fread等讀取均是IO緩慢因?yàn)樗鼈兇蠖鄶?shù)時(shí)間都在“等待”同步 VS 異步同步通信 vs 異步通信(synchronous communication/ asynchronous communication)同步和異步關(guān)注的是消息通信機(jī)制。所謂同步就是在發(fā)出一個(gè)調(diào)用時(shí)在沒(méi)有得到結(jié)果之前該調(diào)用就不返回。但是一旦調(diào)用返回就得到得到結(jié)果了換句話說(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)用阻塞 VS非阻塞阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果消息返回值時(shí)的狀態(tài)。阻塞IO在內(nèi)核將數(shù)據(jù)準(zhǔn)備好之前系統(tǒng)調(diào)用會(huì)一直等待。所有的套接字默認(rèn)都是阻塞的方式。阻塞IO是最常見(jiàn)的IO方式。非阻塞IO如果內(nèi)核還未將數(shù)據(jù)準(zhǔn)備好系統(tǒng)調(diào)用仍會(huì)直接返回并且返回EWOULDBLOCK錯(cuò)誤碼。注意非阻塞IO往往需要程序員反復(fù)嘗試讀寫(xiě)文件描述符這個(gè)過(guò)程稱之為輪詢。這對(duì)CPU來(lái)講是極大的浪費(fèi)一般只有特定場(chǎng)景下才會(huì)使用。fcntlfcntlFile Control是設(shè)置阻塞/非阻塞IO的瑞士軍刀。#include fcntl.h #include unistd.h int fcntl(int fd, int cmd, ...);fd:設(shè)置阻塞/非阻塞的文件對(duì)應(yīng)描述符。cmd可變參數(shù)本質(zhì)上是宏設(shè)置。第二位及以后參數(shù)參數(shù)第二位必須是“命令”像F_GETFL 或 F_SETFL 而第三位必須是“狀態(tài)類型”像O_NONBLOACK。二者絕不可以為了省事而直接按位或全部一次性傳入第二個(gè)參數(shù)常用cmdF_GETFL/F_SETFL獲得/設(shè)置文件描述符狀態(tài)標(biāo)記。O_NONBLOCK設(shè)置非阻塞IOF_DUPFD復(fù)制一個(gè)現(xiàn)有的文件描述符。F_GETFD/F_SETFD獲得/設(shè)置文件描述符標(biāo)記.獲得/設(shè)置異步I/O所有權(quán)(cmdF_GETOWN或F_SETOWN).獲得/設(shè)置記錄鎖(cmdF_GETLK,F_SETLK或F_SETLKW)F_GETFL //獲取fd狀態(tài) 常用來(lái)獲取fd原狀態(tài)F_SETFL //設(shè)置狀態(tài) 常用來(lái)設(shè)置其他狀態(tài)像O_NONBLOCKF_GETFL與F_GETFDF_GETFL(Get File Status Flags)操作的是文件描述符的屬性。它獲取的是該描述符的訪問(wèn)模式只讀、只寫(xiě)、讀寫(xiě)和狀態(tài)標(biāo)志如追加O_APPEND、非阻塞O_NONBLOCK、同步O_SYNC等。這些標(biāo)志是在open()系統(tǒng)調(diào)用時(shí)設(shè)定的并且會(huì)影響后續(xù)的read()、write()操作。F_SETFD(Set File Descriptor Flags)操作的是文件描述符本身的標(biāo)志。它主要設(shè)置的是FD_CLOEXECClose-on-exec標(biāo)志。這個(gè)標(biāo)志不影響讀寫(xiě)只影響當(dāng)進(jìn)程執(zhí)行exec系列函數(shù)時(shí)該描述符是否會(huì)被自動(dòng)關(guān)閉。防止子進(jìn)程繼承不必要的文件描述符安全加固僅有FD_CLOEXEC實(shí)際開(kāi)發(fā)中幾乎只用這個(gè)舉例#includeunistd.h #includefcntl.h void SetNoBlock(int fd) { //1.獲取原fd描述,由于保留原fd狀態(tài)信息 int fl fcntl(fd, F_GETFL); if(fl 0) { perror(fcntl); return; } //2.設(shè)置狀態(tài)標(biāo)記 fcntl(fd, F_SETFL, fl | O_NONBLOCK); }exec系列函數(shù)回顧exec系列是Unix/Linux中替換進(jìn)程映像的系統(tǒng)調(diào)用組。它們的作用是用一個(gè)新的程序替換當(dāng)前進(jìn)程的代碼段、數(shù)據(jù)段、堆和棧但保留進(jìn)程的PID、文件描述符表、信號(hào)處理等。#include unistd.h int execl (const char *path, const char *arg, ..., NULL); int execlp(const char *file, const char *arg, ..., NULL); int execle(const char *path, const char *arg, ..., NULL, char *const envp[]); int execv (const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);// 1. execl逐個(gè)列出參數(shù)以 NULL 結(jié)尾 execl(/bin/ls, ls, -l, /home, NULL); // 2. execlp不用寫(xiě)完整路徑自動(dòng)搜 PATH execlp(ls, ls, -l, /home, NULL); // 3. execle自己指定環(huán)境變量 char *envp[] {PATH/bin:/usr/bin, USERubuntu, NULL}; execle(/bin/ls, ls, -l, /home, NULL, envp);特點(diǎn)返回必然錯(cuò)誤fcntl后read返回值-1read返回-1有兩種可能1.出錯(cuò)像文件描述符關(guān)閉2.數(shù)據(jù)未就緒在非阻塞情況下read后因?yàn)槭欠亲枞x取當(dāng)未及時(shí)發(fā)送數(shù)據(jù)就會(huì)導(dǎo)致“數(shù)據(jù)未就緒”無(wú)論何種情況錯(cuò)誤碼均被設(shè)置錯(cuò)誤碼認(rèn)識(shí) C/C進(jìn)階知識(shí)6.0必要//測(cè)試非阻塞IO #includeiostream #includeunistd.h #includefcntl.h using namespace std; void SetNoBlock (int fd) { int fl fcntl (fd, F_GETFL);//獲取fd的默認(rèn)描述碼 if(fl 0) { perror(fcntl); return; } fcntl (fd, F_SETFL, fl | O_NONBLOCK);//保持fd的原權(quán)限碼非阻塞0 // 注意此處的獨(dú)立 } int main() { // cout sizeof (fd_set) * 8 endl;//1024大小 SetNoBlock (0); while(true) { char buf[1024] { 0 }; int n read (0, buf, 1023); if(n 0) { //正常讀取到了 cout buf; } else if(n 0) { //n 0 //1.出錯(cuò)了 2.底層沒(méi)有數(shù)據(jù)準(zhǔn)備好 if(errno EAGAIN || errno EWOULDBLOCK) //含義均是現(xiàn)在無(wú)數(shù)據(jù)請(qǐng)稍后再試,使用一個(gè)就夠了 { sleep (1); cout 數(shù)據(jù)沒(méi)有準(zhǔn)備好... endl; continue; } else if(errno EINTR ) { cout 被信號(hào)打斷 endl; continue; } //真正讀取出錯(cuò) break; } else { //原為讀到文件(0)末尾。因?yàn)槭墙K端末尾(Ctrld) cout 末尾n: n endl; break; } } }.mp4效果演示fcntl非阻塞IOSelect“一種兼容性極好較為古老的IO模式”select系統(tǒng)調(diào)用是用來(lái)讓我們的程序監(jiān)視多個(gè)文件描述符的狀態(tài)變化的原型#include sys/select.h #include sys/time.h #include sys/types.h #include unistd.h int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);對(duì)于select正常返回后的fd默認(rèn)情況下讀事件不就緒寫(xiě)事件是就緒的。nfds等待的所有文件描述符中最大的值加1后的值。timeout輸入輸出型參數(shù)struct timeval { time_t tv_sec; //單位秒 time_t tv_usec; //單位毫秒 }①nullptr阻塞性一旦有某個(gè)fd就緒就 . ↓ .如果如果當(dāng)時(shí)沒(méi)有就緒的就阻塞。/*了解fd的遍歷*/ // 1. schedule() 返回進(jìn)程重新獲得CPU // 2. 從 do_select_wait() 的 schedule() 后繼續(xù)執(zhí)行 // 3. 移除等待隊(duì)列條目 // 4. 重新遍歷fd集合檢查哪些就緒了 // 5. 返回就緒的fd數(shù)量 // 6. 返回到用戶態(tài)②0非阻塞性僅檢測(cè)一輪若無(wú)就緒就立即返回。不再等待③ 0作為輸入的等待最大時(shí)間容忍度輸出時(shí)輸出被消耗后的剩余時(shí)間。fd_set結(jié)構(gòu)fd_set是一種位圖結(jié)構(gòu)作為輸入輸出型參數(shù)。基于不同的Linux系統(tǒng)可能會(huì)有差異輸入用戶告訴內(nèi)核位圖上為1的值對(duì)應(yīng)的下標(biāo)就是你要關(guān)心的相應(yīng)的 fd 值。輸出表示關(guān)心的fd內(nèi)已經(jīng)狀態(tài)就緒設(shè)置為1的fd是哪些。此處的“狀態(tài)”指參數(shù)位于參數(shù)對(duì)應(yīng)的狀態(tài)情況分別是讀、寫(xiě)、異常。返回值 r當(dāng)新鏈接到達(dá)后也是“立即返回”返回值 0 。r 0有 r 個(gè) fd 就緒了。r 0超時(shí)。因?yàn)楫?dāng)為nullptr時(shí)為阻塞性所以沒(méi)有這種情況r 0出錯(cuò)像打開(kāi)一個(gè)從未打開(kāi)的文件描述符注意select由于資歷較老所以其兼容性極好。Select通信原理實(shí)現(xiàn)了單進(jìn)程/線程多路IO0.怎么解決單進(jìn)程下復(fù)原問(wèn)題三狀態(tài)參數(shù)加入輔助數(shù)組 int fd_array[8*sizeof(fd_set)] -1;select前將上一輪或許新納入了的fd納入狀態(tài)參數(shù)。1.怎么區(qū)分新鏈接與信息答根據(jù)三狀態(tài)參數(shù) 均是輸入輸出型參數(shù)可知若是新鏈接則一定select后對(duì)應(yīng)輔助數(shù)組參數(shù)就緒因此應(yīng)先判斷l(xiāng)istenfd對(duì)應(yīng)的fd_array[0]是否就緒。就緒新鏈接到來(lái)。因?yàn)閘istenfd是監(jiān)聽(tīng)fd未就緒信息到來(lái)注意創(chuàng)建fd_set后一定要第一時(shí)間初始化FD_ZERO()Echo Server必看核心是Dispatch函數(shù)的因果邏輯實(shí)現(xiàn)基于select的echo server注意TCP與UDP的差異測(cè)試代碼int n select (_sockfd 1, rfds, nullptr, nullptr, nullptr);//阻塞性 if(n 0) { //新鏈接到來(lái)/鏈接發(fā)送了信息!!! // Dispatch (buf); // 派遣 cout test; continue; }我發(fā)了一個(gè)信息為什么一直打印不應(yīng)該打印一次后阻塞嗎答因?yàn)闆](méi)有服務(wù)端沒(méi)有將接收緩沖區(qū)內(nèi)的數(shù)據(jù)讀走意識(shí)read不僅僅是拷貝還將接收緩沖區(qū)內(nèi)數(shù)據(jù)清除監(jiān)聽(tīng)套接字是用來(lái)監(jiān)聽(tīng)是否有新鏈接的不能作為通信套接字否則FD_ISSETFD_ISSET的底層含位運(yùn)算使用fd參數(shù)不可以帶負(fù)數(shù)。第一個(gè)參數(shù)fdFD_ISSET的第一個(gè)參數(shù)可能是-1無(wú)法進(jìn)行位運(yùn)算正確判斷//正確寫(xiě)法 if(FD_ISSET (fd_array[i], rfds)) //錯(cuò)誤寫(xiě)法 // if (FD_ISSET (fd_array[i], rfds) ! 0) // 在集合rfds中返回非零 // if (FD_ISSET (fd_array[i], rfds) true) // 在集合rfds中返回非零注意是輸入輸出型參數(shù)。select特點(diǎn)與缺點(diǎn)特點(diǎn)1.可檢測(cè)文件描述符的個(gè)數(shù)取決于sizeof(fd_set)值。ubuntu賬號(hào)sizeof(fd_set) 128每字節(jié)8比特共計(jì)128*8 1024個(gè)文件描述符。2.將需要監(jiān)控狀態(tài)的fdrfds wfds efds符加入select監(jiān)控后常需額外一個(gè)數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)歷史關(guān)心情況。3.每次調(diào)用select都會(huì)將rfd/wfd/efd集合從用戶態(tài)拷貝到內(nèi)核態(tài)這個(gè)開(kāi)銷在fd很多時(shí)會(huì)大很多。缺點(diǎn)1.每次調(diào)用select都需要手動(dòng)設(shè)置fd集合很不方便。2.select支持的文件描述符太少。下節(jié)我們將講述poll的使用感謝支持長(zhǎng)期連載歡迎關(guān)注