高級IO
高級IO所涉及的不僅僅是普通的數據讀寫,而是與數據讀寫(IO)有關的一些高級操作。
博客高級IO涉及:
(1)非阻塞IO
(2)記錄鎖(文件鎖)
(3)io多路複用(I/O multiplexing)
(4)異步IO
(5)存儲映射
高級IO所有的內容都與文件的IO有關(數據讀寫),只要涉及到文件的IO操作,就必然有文件描述符,所以博客所有的IO高級操作,都是使用fd來實現的。
除了第5個“存儲映射”外,其它高級IO操作都必須依賴fcntl函數的支持,所以對於本博客來說,fcntl函數很重要。
非阻塞IO
阻塞讀文件
讀某些文件時,如果文件沒有數據的話,往往會導致讀操作會阻塞(休眠)。
比如:
(1)讀鼠標、鍵盤等字符設備文件
(2)讀管道文件(有名無名)
我們這裏只演示讀鼠標、鍵盤,關於管道文件的讀取,讀者可以閱讀管道文件的博客。
代碼演示:
阻塞讀鼠標和鍵盤
阻塞讀鍵盤
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
char buf[100] = {0};
int ret;
while(1)
{
printf("please input :\n");
ret = read(0,buf,sizeof(buf));
if(ret >0)printf("%s\n",buf);
}
return 0;
}
運行結果爲:
阻塞讀鼠標
鼠標需要我們自己調用open函數打開。
鼠標文件的存放位置:
讀取鼠標的時候讀取到的是這個光標所在的座標,是一個整型值。
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
int cor;
int mousefd = -1;
int ret = 0;
mousefd = open("/dev/input/mouse0",O_RDONLY);
if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);
while(1)
{
printf("please input:\n");
ret = read(mousefd,&cor,sizeof(cor));
if(ret>0) printf("%d\n",cor);
}
return 0;
}
運行結果爲:
我們可以看到打開鼠標失敗,沒有權限。
上面是在stu用戶下面運行,但是前面我們可以看到mouse0文件在stu用戶下面是沒有運行權限的。
所以我們在管理員用戶下面運行。
上面運行結果是我鼠標在不斷移動的過程中產生的光標整型值,如果沒有讀取到光標值,就會阻塞。
讀普通文件會阻塞嗎?
讀普通文件時,如果讀到了數據就成功返回,如果沒有讀到數據返回0,返回0並不是代表出錯返回,總之不會阻塞。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
int cor;
//int mousefd = -1;
int ret = 0;
char buf[100] = {0};
//mousefd = open("/dev/input/mouse0",O_RDONLY);
//if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);
int fd = open("./fail",O_RDONLY|O_CREAT,0664);
if(fd == -1) print_err("open ./file fail",__LINE__,errno);
while(1)
{
printf("please input:\n");
ret = read(fd,buf,sizeof(buf));
if(ret>0) printf("%s\n",buf);
printf("%d\n",ret);
printf("input over\n");
}
return 0;
}
運行結果爲:
我們可以看到會循環的打印,並且打印讀取到的字節數0,讀取普通文件不會阻塞。
沒有數據不會出錯返回,沒有數據只是返回0,只是說明讀取到的數據沒有,讀取到的字節數爲0。
總之,不管有沒有從普通文件讀到數據,讀普通文件時都不會阻塞。
寫文件時會阻塞嗎?
在寫某些文件時,當文件不能立即接收寫入的數據時,也可能會導致寫操作阻塞,一直阻塞到寫成功爲止。
一般來說,寫文件不會阻塞,因此我們不考慮寫文件阻塞的情況,我們這裏只說明讀文件阻塞的情況。
阻塞是好還是壞
實際上讀文件因爲沒有數據而阻塞,其實是好事,因爲這樣子就進入休眠狀態,休眠時就不會佔用CPU,節省了cpu的資源。
我能不能將阻塞的讀修改爲非阻塞的讀呢?
答:可以,非阻塞讀的意思就是說,如果有數據就成功讀到,如果沒有讀到數據就出錯返回,而不是阻塞。
既然阻塞讀很好,爲什麼提供非阻塞讀呢?
答:儘管我們很少非阻塞的讀,因爲浪費更多CP資源,但是有些時候還真需要非阻塞的讀,因此OS還是爲我們提供了非阻塞操作方式。
如何實現非阻塞讀
打開文件時指定O_NONBLOCK狀態標誌實現非阻塞
fd = open("/dev/input/mouse0", O_RDONLY|O_NONBLOCK);
if(fd < 0)
{
perror("open /dev/input/mouse1 is fail");
exit(-1);
}
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
int cor;
int mousefd = -1;
int ret = 0;
char buf[100] = {0};
mousefd = open("/dev/input/mouse0",O_RDONLY|O_NONBLOCK);
if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);
while(1)
{
printf("please input:\n");
ret = read(mousefd,&cor,sizeof(cor));
if(ret>0) printf("%d\n",cor);
printf("%d\n",ret);
printf("input over\n");
}
return 0;
}
運行結果爲:
這個過程首先是讀數據,如果有數據就進行打印,如果沒有數據read函數不會阻塞,會出錯返回,進入下一輪讀取,我們就可以看到即使鼠標不移動,也會不斷打印。
如果我們進行報錯,我們來查看運行結果:
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
int cor;
int mousefd = -1;
int ret = 0;
char buf[100] = {0};
mousefd = open("/dev/input/mouse0",O_RDONLY|O_NONBLOCK);
if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);
while(1)
{
printf("please input:\n");
ret = read(mousefd,&cor,sizeof(cor));
if(ret>0) printf("%d\n",cor);
else if(ret == -1) print_err("read mouse0 fail",__LINE__,errno);
printf("%d\n",ret);
printf("input over\n");
}
return 0;
}
運行結果:
我們可以看到讀取的時候會直接報錯,因爲沒有數據的時候是出錯返回返回-1,那麼我們實現非阻塞,不要讓其返回退出,那麼我們就可以把因爲非阻塞而導致的錯誤排除掉,讓非阻塞意外的其他錯誤進行報錯。
非阻塞錯誤:
我們通過man 2 read
找到錯誤號:errno
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
int cor;
int mousefd = -1;
int ret = 0;
char buf[100] = {0};
mousefd = open("/dev/input/mouse0",O_RDONLY|O_NONBLOCK);
if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);
while(1)
{
printf("please input:\n");
ret = read(mousefd,&cor,sizeof(cor));
if(ret>0) printf("%d\n",cor);
else if(ret == -1&&errno!=EAGAIN) print_err("read mouse0 fail",__LINE__,errno);
printf("%d\n",ret);
printf("input over\n");
}
return 0;
}
運行結果爲:
那麼就不會出現報錯。
在說明本機IPC有名管道時,如果不希望阻塞的話,就可以在open打開“有名管道”時,指定O_NONBLOCK,然後讀有名管道無數據時就不會阻塞。
通過fcntl函數指定O_NONBLOCK來實現非阻塞
什麼情況下會使用fcntl來實現:
情況1:當文件已經被open打開了,但是open是並沒有指定你要的文件狀態標誌,而你又不想去修改open的參數,此時就是可以使用fcntl來重設或者補設。
情況2:沒辦法在open指定,你手裏只有一個文件描述符fd,此時就使用fcntl來重設或者補設比如無名管道,無名管道連名字都沒有,沒辦法使用open函數,無名管道是使用pipe函數來返回文件描述符的,如果過你想非阻塞的讀無名管道的話,是沒有辦法通過open來指定O_NONBLOCK的,此時就需要使用fcntl來重設或者補設。
當然我們使用fcntl不僅僅只能重設或者補設O_NONBLOCK,也可以重設或者補設O_TRUNC/O_APPEND等任何你需要的“文件狀態”標誌。
使用fcntl函數的例子:將0文件描述符設置爲O_NONBLOCK。
設置有兩種方式:
· 重設
講0文件描述符設置爲非阻塞。
之前打開標準輸入文件的時候,設置的是以只讀文件打開的。那麼我們就可以重新設置。
fcntl(0, F_SETFL, O_RDONLY|O_NONBLOCK);
上面就是設置文件狀態標誌,把以前的文件描述符爲0的文件狀態標誌使用
O_RDONLY|O_NONBLOCK
進行替換。
· 補設,在原來文件狀態標誌的基礎之上進行補設。
flag = fcntl(0, F_GETFL); //獲取原有文件狀態標誌
flag = flag | O_NONBLOCK; //通過|操作,在已有的標誌上增設O_NONBLOCK
fcntl(0, F_SETFL, flag); //將修改後的“文件狀態標誌”設置回去
重設代碼演示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
int ret = 0;
char buf[100] = {0};
//重設 0
fcntl(0, F_SETFL, O_RDONLY|O_NONBLOCK);
while(1)
{
printf("please input:\n");
ret = read(0,buf,sizeof(buf));
if(ret>0) printf("%s\n",buf);
printf("input over\n");
}
return 0;
}
運行結果爲:
CPU會輪循的讀。我們可以看到有數據就會讀取並且打印,沒有數據,進入下一輪循環重新讀取,進而實現非阻塞。
補設代碼演示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
//int cor;
//int mousefd = -1;
int ret = 0;
char buf[100] = {0};
int flag = 0;
//mousefd = open("/dev/input/mouse0",O_RDONLY|O_NONBLOCK);
//if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);
//reset 0
flag = fcntl(0, F_GETFL); //獲取原有文件狀態標誌
flag = flag | O_NONBLOCK; //通過|操作,在已有的標誌上增設O_NONBLOCK
fcntl(0, F_SETFL, flag); //將修改後的“文件狀態標誌”設置回去
while(1)
{
printf("please input:\n");
ret = read(0,buf,sizeof(buf));
if(ret>0) printf("%s\n",buf);
//else if(ret == -1&&errno!=EAGAIN) print_err("read mouse0 fail",__LINE__,errno);
//printf("%d\n",ret);
printf("input over\n");
}
return 0;
}
運行結果爲:
我們可以通過補設實現非阻塞。
實現同時“讀鼠標”和“讀鍵盤”
由於一般情況下,“讀鼠標”和“讀鍵盤”都是阻塞的,爲了不要讓“讀鼠標”和“讀鍵盤”因爲阻塞而相互干擾,可以採取如下辦法來讀。
(1)fork子進程,然後父子進程兩線任務
父進程:讀鍵盤
子進程:讀鼠標
讀者可以自行實現或者參考,fork函數的用法參考進程控制博客。
上面這種方式肯定是沒問題的,但是這裏僅僅只是告訴你這樣可以,但是並不主張大家這麼做,因爲我們說過,多線任務時不要使用多進程來實現,開銷太大。
(2)創建次線程,主線程和次線程兩線任務
主線程:讀鍵盤
次線程:讀鼠標
這纔是我們經常實現的方式。
請參考線程自行實現。
3)將鼠標和鍵盤設置爲“非阻塞”,while輪詢的讀。
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
int cor;
int mousefd = -1;
int ret = 0;
char buf[100] = {0};
int flag = 0;
mousefd = open("/dev/input/mouse0",O_RDONLY|O_NONBLOCK);
if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);
//補設文件狀態標誌
flag = fcntl(0, F_GETFL); //獲取原有文件狀態標誌
flag = flag | O_NONBLOCK; //通過|操作,在已有的標誌上增設O_NONBLOCK
fcntl(0, F_SETFL, flag); //將修改後的“文件狀態標誌”設置回去
while(1)
{
printf("please input:\n");
ret = read(mousefd,&cor,sizeof(cor));
if(ret > 0)printf("%d\n",cor);
else if(ret == -1&&errno!=EAGAIN) print_err("read mouse0 fail",__LINE__,errno);
ret = read(0,buf,sizeof(buf));
if(ret>0) printf("%s\n",buf);
printf("input over\n");
}
return 0;
}
運行結果爲:
我們可以看到兩個都是非阻塞的,因爲但凡有一個是阻塞的就不會連續打印。
刪掉兩個while循環裏面的兩個printf
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
int cor;
int mousefd = -1;
int ret = 0;
char buf[100] = {0};
int flag = 0;
mousefd = open("/dev/input/mouse0",O_RDONLY|O_NONBLOCK);
if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);
//reset 0
flag = fcntl(0, F_GETFL); //獲取原有文件狀態標誌
flag = flag | O_NONBLOCK; //通過|操作,在已有的標誌上增設O_NONBLOCK
fcntl(0, F_SETFL, flag); //將修改後的“文件狀態標誌”設置回去
while(1)
{
ret = read(mousefd,&cor,sizeof(cor));
if(ret > 0)printf("%d\n",cor);
else if(ret == -1&&errno!=EAGAIN) print_err("read mouse0 fail",__LINE__,errno);
ret = read(0,buf,sizeof(buf));
if(ret>0) printf("%s\n",buf);
}
return 0;
}
運行結果爲:
我們就可以實現在不斷移動鼠標的同時,可以多次輸入數據進行打印。
使用非阻塞的方式CPU會不斷的輪詢,非常消耗CPU資源,所以我們一般使用多線程來實現,所以只有當某些情況下我們不得不使用非阻塞的時候我們纔會使用非阻塞。
補充
把0設置爲非阻塞,使用scanf從鍵盤讀數據時阻塞的嗎?
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void print_err(char *str,int line,int err_no)
{
printf("%d,%s:%s\n",line,str,strerror(err_no));
exit(-1);
}
int main()
{
int cor;
int ret = 0;
char buf[100] = {0};
int flag = 0;
flag = fcntl(0, F_GETFL); //獲取原有文件狀態標誌
flag = flag | O_NONBLOCK; //通過|操作,在已有的標誌上增設O_NONBLOCK
fcntl(0, F_SETFL, flag); //將修改後的“文件狀態標誌”設置回去
while(1)
{
printf("input\n");
ret = scanf("%s",buf);
if(ret>0) printf("%s\n",buf);
printf("input over\n");
}
return 0;
}
運行結果爲:
我們可以看到循環打印,所以也是非阻塞的。
我們仍然把上面代碼中的兩個printf去掉之後運行代碼,運行結果爲:
爲什麼scanf也不阻塞?
scanf()是調用read(0, …)來實現的,scanf的阻塞是因爲調用read阻塞讀0描述符導致的,既然已經將0改爲了非阻塞,read讀0就不再阻塞,自然scanf也就不再阻塞。
所以如果你不想scanf阻塞的話,就可以將0描述符設置爲非阻塞。
當然我這個話只能針對Linux,因爲fcntl函數是Linux的系統函數,不是標準C庫函數,這個函數在windows那邊不一定支持,在windows等系統下,如何設置非阻塞,他們的系統API可能不一樣。
不過對於c的標準IO函數scanf來說,都是希望阻塞讀的,基本不會遇到需要將scanf改爲非阻塞的情況。