1. 程式人生 > >(51)LINUX應用編程和網絡編程之六Linux高級IO

(51)LINUX應用編程和網絡編程之六Linux高級IO

linu read 簡單 長度 更新 非阻塞 argv 應用程序 事情

3.6.1.非阻塞IO 3.6.1.1、阻塞與非阻塞 阻塞:阻塞具有很多優勢(是linux系統的默認設置),單路IO的時候使用阻塞式IO沒有降低CPU的性能 補充:阻塞/非阻塞, 它們是程序在等待消息(無所謂同步或者異步)時的狀態. 阻塞調用是指調用結果返回之前,當前線程會被掛起。函數只有在得到結果之後才會返回。 有人也許會把阻塞調用和同步調用等同起來,實際上他是不同的。 對於同步調用來說,很多時候當前線程還是激活的,只是從邏輯上當前函數沒有返回而已。 非阻塞和阻塞的概念相對應,指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。 3.6.1.2、為什麽有阻塞式 (1)常見的阻塞:wait(顯式回收子進程)、pause、sleep等默認阻塞函數;read或write某些文件時也是默認阻塞式的
(2)阻塞式的好處:當前線程被掛起等待條件滿足才返回結果 3.6.1.3、非阻塞 (1)為什麽要實現非阻塞 (2)如何實現【非阻塞IO訪問】: 1)打開文件時加入O_NONBLOCK 2)fcntl函數,對文件描述符(文件已經打開)進行操作 單路IO就用阻塞式比較好; 多路IO最好是用非阻塞式的。避免資源被一個占有不放,讓其他IO有資源可搶。 從CPU的利用角度來看,單路IO就是一對一;多路IO就是一對多,即一個CPU對應多個資源搶占通道。前者單路效率更高,後者需要兼顧分配。單路IO模型只需要監聽一個IO流,多路IO模型可以同時監聽(內部輪循)多個IO流,CPU要不停去查看。
3.6.2.阻塞式IO的困境 3.6.2.1、程序中讀取鍵盤 3.6.2.2、程序中讀取鼠標 cat /dev/input/mouse1 3.6.2.3、程序中同時讀取鍵盤和鼠標 3.6.2.4、問題分析 並不是所有的情況下都適阻塞式io的。 代碼示例: #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> /* 程序中讀取鍵盤和鼠標 */ #define NAME "/dev/input/mouse1"
char buff[100]={0}; char buf[100]={0}; int main(int argc,char **argv) { int ssize_t=-1; int fd=-1; fd=open(NAME,O_RDWR); if(-1==fd) { perror("open"); _exit(-1); } printf("打開成功!fd=%d\n",fd); while(1){ ssize_t=read(fd,buff,sizeof(buff)); if(-1==ssize_t) { perror("read"); _exit(-1); } printf("讀到的鼠標字符數為%d\n",ssize_t); printf("讀到的鼠標字符是[%s]\n",buff); read(0,&buf,sizeof(buf)); printf("讀到的鼠標字符是[%s]\n",buf); sleep(1); } return 0; } 3.6.3.並發式IO的3種解決方案【並發式IO】 3.6.3.1、非阻塞式IO:性能不夠好,有點類似於輪詢的方式,CPU不停的去查看 代碼示例: #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define NAME "/dev/input/mouse1" char buff[100]={0}; //鍵盤 char buf[100]={0};//鼠標 int main(void) { int ssize_t=-1; int fd=-1; int flag=-1; int ret=-1; fd=open(NAME,O_RDWR | O_NONBLOCK ); if(-1==fd) { perror("open"); _exit(-1); } //把標準輸入文件描述符0通過fcntl函數變成非阻塞式子 // 把0號文件描述符(stdin)變成非阻塞式的 flag = fcntl(0, F_GETFL); // 先獲取原來的flag flag |= O_NONBLOCK; // 添加非阻塞屬性 fcntl(0, F_SETFL, flag); // 更新flag // 這3步之後,0就變成了非阻塞式的了 while (1) { // 讀鼠標 memset(buf, 0, sizeof(buf)); //printf("before 鼠標 read.\n"); ret = read(fd, buf, 50); if (ret > 0) { printf("鼠標讀出的內容是:[%s].\n", buf); } // 讀鍵盤 memset(buff, 0, sizeof(buff)); //printf("before 鍵盤 read.\n"); ret = read(0, buff, 5); if (ret > 0) { printf("鍵盤讀出的內容是:[%s].\n", buff); } } return 0; } 3.6.3.2、多路復用IO :性能相對比較好,解決並發性IO的解決 3.6.4.IO多路復用原理 3.6.4.1、何為IO多路復用 【說白了,多路復用其實就是一個管多個】 (1)IO multiplexing (2)用在什麽地方?多路非阻塞式IO(多路及時響應)。 (3)select和poll兩個函數:(poll出現的比較晚一點。其性能也要比select函數的性能更好一些) (4)外部阻塞式(select和poll兩個函數本身在調用的時候是阻塞式的,對外表現為阻塞式),內部非阻塞式( 兩個函數內部實現的時候,也就是當把要監聽的東西(比如A和B兩個阻塞式IO)放在這個函數的監聽範圍)【自動】輪詢【這兩個多路阻塞式IO】 (輪詢的意思就是CPU不停的去查看) 補充: 可是使用Select就可以完成非阻塞(所謂非阻塞方式non-block,就是進程或線程執行此函數時不必非要 等待事件的發生,一旦執行肯定返回,以返回值的不同來反映函數的執行情況,如果事件發生則與阻塞方式相同,若事件沒有發生則返回一個代碼來告知事件未發生,而進程或線程繼續執行,所以效率較高)方式工作的程序,它能夠監視我們需要監視的文件描述符的變化情況——讀寫或是異常。 3.6.4.2、select函數介紹: #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); //nfds表示文件描述符的個數 nfds is the highest-numbered file descriptor in any of the //three sets, plus 1,是指集合中所有文件描述符的範圍,【即所有文件描述符的最大值加1】這一點要註意,所有文件描述符的最大值加1,比如一個文件描述符是0,一個文件描述符是1,則為1+1 相關函數: (1)void FD_ZERO(fd_set *set); //把文件描述符集合清零 (2)void FD_SET(int fd, fd_set *set); //把某個文件描述符添加到這個集合中去 (3)int FD_ISSET(int fd, fd_set *set); //檢查集合中指定的文件描述符是否可以讀寫 FD_ISSET() tests to see if a file descriptor is part of the set; this is useful after select() returns. (4)void FD_CLR(int fd, fd_set *set); //把一個給定的文件描述符從集合中刪除 相關結構體: (1)struct fd_set可以理解為一個集合,這個集合中存放的是文件描述符(file descriptor)。 (2) struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; struct timeval* timeout是select的超時時間,這個參數至關重要,它可以使select處於三種狀態,第一,若將NULL以形參傳入,即不傳入時間結構,就是將select置於阻塞狀態,一定等到監視文件描述符集合中某個文件描述符發生變化為止;第二,若將時間值設為0秒0毫秒,就變成一個純粹的非阻塞函數,不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;第三,timeout的值大於0,這就是等待的超時時間,即select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回,返回值同上述。 linux內部代碼示例: #include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int main(void) { fd_set rfds; struct timeval tv; int retval; /* Watch stdin (fd 0) to see when it has input. */ FD_ZERO(&rfds); //先把文件描述符集合清零 FD_SET(0, &rfds); //把0文件描述符添加到這個集合中去 /* Wait up to five seconds. */ tv.tv_sec = 5; tv.tv_usec = 0; retval = select(1, &rfds, NULL, NULL, &tv); /* Don‘t rely on the value of tv now! */ if (retval == -1) perror("select()"); else if (retval) printf("Data is available now.\n"); /* FD_ISSET(0, &rfds) will be true. */ else printf("No data within five seconds.\n"); exit(EXIT_SUCCESS); } 3.6.4.3、poll函數介紹 #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); #define _GNU_SOURCE /* See feature_test_macros(7) */ 結構體: struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ }; 代碼示例: #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <poll.h> /* #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); */ #define NAME "/dev/input/mouse1" int main(void) { char buf[100]={0}; //註意這裏的定義的buf不能夠使用全局的 //int poll(struct pollfd *fds, nfds_t nfds, int timeout); struct pollfd a[2]={0}; int fd= open(NAME,O_RDWR); //定義鼠標文件描述符 if(fd<0) { perror("open mouse"); _exit(-1); } //實例化結構體 a[0].fd=fd; //鼠標 a[0].events=POLLIN; a[1].fd=0; //鍵盤 a[1].events=POLLIN; int ret=poll(a,fd+1,10000); if(ret<0) { perror("poll"); _exit(-1); } if(ret==0) { printf("超時了"); } else //判斷是誰發生了IO { if(a[0].events==a[0].revents) //鼠標 { memset(buf,0,sizeof(buf)); read(fd,buf,10); printf("讀出來的鼠標內容是:[%s]\n",buf); } if(a[1].events==a[1].revents) //鍵盤 { memset(buf,0,sizeof(buf)); read(0,buf,50); printf("讀出來的鍵盤內容是:[%s]\n",buf); } } return 0; } 3.6.5.IO多路復用實踐 3.6.5.1、用select函數實現同時讀取鍵盤鼠標 select()函數實現IO多路復用的步驟 (1)清空描述符集合 (2)建立需要監視的描述符與描述符集合的關系 (3)調用select函數 (4)檢查監視的描述符判斷是否已經準備好 (5)對已經準備好的描述符進程IO操作 代碼示例: /* int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); */ #include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define NAME "/dev/input/mouse1" char buf[100]={0}; int main(void) { int fd=-1; fd_set readfds=-1; struct timeval time; int ret=-1; fd=open(NAME,O_RDWR); printf("fd=%d\n",fd); if(-1==fd) { perror("open"); _exit(-1); } FD_ZERO(&readfds); //清除文件描述符集合 FD_SET(fd,&readfds); FD_SET(0,&readfds); time.tv_sec=5; //設置時間為5秒 time.tv_usec=0; ret=select(3,&readfds,NULL,NULL,&time); //調用select函數 ,註意第一個參數 if (ret < 0) //表示出錯 { perror("select: "); return -1; } else if (ret == 0) //表示超過了規定的時間限制 { printf("超時了\n"); } else { // 等到了一路IO,然後去監測到底是哪個IO到了,處理之 if (FD_ISSET(0, &readfds)) { // 這裏處理鍵盤 memset(buf, 0, sizeof(buf)); read(0, buf, 5); printf("鍵盤讀出的內容是:[%s].\n", buf); } if (FD_ISSET(fd, &readfds)) { // 這裏處理鼠標 memset(buf, 0, sizeof(buf)); read(fd, buf, 50); printf("鼠標讀出的內容是:[%s].\n", buf); } } return 0; } 3.6.5.2、用poll函數實現同時讀取鍵盤鼠標 poll函數介紹: #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); 第一個參數 pollfd 結構體定義如下: 引用 /* Data structure describing a polling request. */ struct pollfd { int fd; /* poll 的文件描述符. */ short int events; /* fd 上感興趣的事件(等待的事件或者說是監視的事件). */ short int revents; /* fd 上實際發生的事件. */ }; fd 成員表示感興趣的,且打開了的文件描述符; events 成員是位掩碼,用於指定針對這個文件描述符感興趣的事件; revents 成員是位掩碼,用於指定當 poll 返回時,在該文件描述符上已經發生了哪些事情。 events 和 revents 結合下列常數值(宏)指定即將喚醒的事件或調查已結束的 poll() 函數被喚醒的原因,這些宏常數如下: POLLIN events 中使用該宏常數,能夠在折本文件的可讀情況下,結束 poll() 函數。相反,revents 上使用該宏常數,在檢查 poll() 函數結束後,可依此判斷設備文件是否處於可讀狀態(即使消息長度是 0)。 POLLPRI 在 events 域中使用該宏常數,能夠在設備文件的高優先級數據讀取狀態下,結束 poll() 函數。相反,revents 上使用該宏常數,在檢查 poll() 函數結束後,可依此判斷設備文件是否處於可讀高優先級數據的狀態(即使消息長度是 0)。該宏常數用於處理網絡信息包(packet) 的數據傳遞。 POLLOUT 在 events 域中使用該宏常數,能夠在設備文件的寫入狀態下,結束 poll() 函數。相反,revents 域上使用該宏常數,在檢查 poll() 結束後,可依此判斷設備文件是否處於可寫狀態。 POLLERR 在 events 域中使用該宏常數,能夠在設備文件上發生錯誤時,結束 poll() 函數。相反,revents 域上使用該宏函數,在檢查 poll() 函數結束後,可依此判斷設備文件是否出錯。 POLLHUP 在 events 域中使用該宏常數,能夠在設備文件中發生 hungup 時,結束 poll() 函數 。相反,在檢查 poll() 結束後,可依此判斷設備文件是否發生 hungup 。 POLLNVAL 在 events 域中使用該宏函數,能夠在文件描述符的值無效時,結束 poll() 。相反,在 revents 域上使用該宏函數時,在檢查 poll() 函數後,文件描述符是否有效。可用於處理網絡信息時,檢查 socket handler 是否已經無效。 最後一個參數 timeout 指定 poll() 將在超時前等待一個事件多長事件。這裏有 3 種情況: 1) timeout 為 -1 這會造成 poll 永遠等待。poll() 只有在一個描述符就緒時返回,或者在調用進程捕捉到信號時返回(在這裏,poll 返回 -1),並且設置 errno 值為 EINTR 。-1 可以用宏定義常量 INFTIM 來代替(在 pth.h 中有定義) 。 2) timeout 等於0 在這種情況下,測試所有的描述符,並且 poll() 立刻返回。這允許在 poll 中沒有阻塞的情況下找出多個文件描述符的狀態。 3) time > 0 這將以毫秒為單位指定 timeout 的超時周期。poll() 只有在超時到期時返回,除非一個描述符變為就緒,在這種情況下,它立刻返回。如果超時周期到齊,poll() 返回 0。這裏也可能會因為某個信號而中斷該等待。 和 select 一樣,文件描述符是否阻塞對 poll 是否阻塞沒有任何影響。 代碼示例: #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <poll.h> #define NAME "/dev/input/mouse1" int main(void) { char buf[100]={0}; //註意這裏的定義的buf不能夠使用全局的 //int poll(struct pollfd *fds, nfds_t nfds, int timeout); struct pollfd a[2]={0}; int fd= open(NAME,O_RDWR); //定義鼠標文件描述符 if(fd<0) { perror("open mouse"); _exit(-1); } //實例化結構體 a[0].fd=fd; //鼠標 a[0].events=POLLIN; a[1].fd=0; //鍵盤 a[1].events=POLLIN; int ret=poll(a,fd+1,10000); if(ret<0) { perror("poll"); _exit(-1); } if(ret==0) { printf("超時了"); } else //判斷是誰發生了IO { if(a[0].events==a[0].revents) //鼠標 { memset(buf,0,sizeof(buf)); read(fd,buf,10); printf("讀出來的鼠標內容是:[%s]\n",buf); } if(a[1].events==a[1].revents) //鍵盤 { memset(buf,0,sizeof(buf)); read(0,buf,50); printf("讀出來的鍵盤內容是:[%s]\n",buf); } } return 0; } 3.6.6.異步IO 3.6.6.1、何為異步IO (1)幾乎可以認為:異步IO就是操作系統用軟件實現的一套中斷響應系統(有點類似與硬件中斷)。 有兩種類型的文件IO同步:同步文件IO和異步文件IO。異步文件IO也就是重疊IO。【在同步文件IO中,線程啟動一個IO操作然後就立即進入等待狀態,直到IO操作完成後才醒來繼續執行。而異步文件IO方式中,線程發送一個IO請求到內核,然後繼續處理其他的事情,內核完成IO請求後,將會通知線程IO操作完成了。】 如果IO請求需要大量時間執行的話,異步文件IO方式可以顯著提高效率,因為在線程等待的這段時間內,CPU將會調度其他線程進行執行,如果沒有其他線程需要執行的話,這段時間將會浪費掉(可能會調度操作系統的零頁線程)。如果IO請求操作很快,用異步IO方式反而還低效,還不如用同步IO方式。 同步IO在同一時刻只允許一個IO操作,也就是說對於同一個文件句柄的IO操作是序列化的,即使使用兩個線程也不能同時對同一個文件句柄同時發出讀寫操作。重疊IO允許一個或多個線程同時發出IO請求。 異步IO在請求完成時,通過將文件句柄設為有信號狀態來通知應用程序,或者應用程序通過GetOverlappedResult察看IO請求是否完成,也可以通過一個事件對象來通知應用程序。 簡單的說“同步在編程裏,一般是指某個IO操作執行完後,才可以執行後面的操作。異步則是,將某個操作給系統,主線程去忙別的事情,等內核完成操作後通知主線程異步操作已經完成。” (2)異步IO的工作方法是:我們當前進程向內核註冊一個異步IO事件(使用signal註冊一個信號SIGIO的處理函數),然後當前進程可以正常處理自己的事情,內核就去幫你完成或者說是檢測你希望的事件是否發生,當異步事件發生後當前進程會收到內核傳來的一個SIGIO信號(類似於中斷信號)從而執行綁定的處理函數去處理這個異步事件。 3.6.6.2、涉及的函數: (1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN) 設置異步通知 (2)signal或者sigaction函數(SIGIO) 3.6.3.代碼實踐 3.6.7.存儲映射IO 存儲映射IO使一個磁盤文件(物理地址)與存儲空間(內存地址)中的一個緩沖區相映射。於是當從緩沖區取數據,就相當於讀文件中的相應字節。與此類似,將數據存入緩沖區,則相應字節就自動地寫入文件。這樣就可以在不使用read和write的情況下執行IO。為了使用這種功能,應首先告訴內核將一個給定的文件映射到一個存儲區域中,這是由mmap函數實現的。 3.6.7.1、mmap函數:把一個磁盤文件和一個內存映射起來 內存映射函數 3.6.7.2、LCD顯示和IPC之共享內存 3.6.7.3、存儲映射IO的特點 (1)共享而不是復制,減少內存操作 (2)處理大文件時效率高,小文件不劃算(視頻用到的也比較多) 函數原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); addr參數用於指定映射存儲區的起始地址,通常將其設置為0,這表示由系統選擇該映射區的起始地址,此函數的返回地址是該映射區的起始地址。 fd指定要被映射文件的描述符,在映射該文件到一個地址空間之前,先要打開該文件。 length是映射的字節數。 offset是要映射字節在文件中的起始偏移量。 prot參數說明對映射存儲區的保護要求,但不能超過文件open模式訪問權限,prot可選值如下: PROT_EXEC Pages may be executed. PROT_READ Pages may be read. PROT_WRITE Pages may be written. PROT_NONE Pages may not be accessed. flags參數影響映射存儲區的多種屬性,其中MAP_SHARED和MAP_PRIVATE兩者必須選擇其一,還有許多其它的MAP_XXX是可選的。 【還有一種就是多進程下,父進程fork創建一個子進程來處理不同的事情】。

(51)LINUX應用編程和網絡編程之六Linux高級IO