(51)LINUX應用編程和網絡編程之六Linux高級IO
阿新 • • 發佈:2017-09-29
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