Linux-應用編程-學習筆記(18):對於阻塞式IO困境的解決
前言:內核默認的IO狀態基本都爲阻塞式,這是因爲通過阻塞式的方式能夠發揮操作系統的性能,讓CPU時刻工作在被需要的情況下。但是隻是單純的阻塞式設計可能會帶來一些危害,所以如何設計一種IO多路複用的狀態是非常重要的。
一、阻塞式IO
1. 非阻塞式IO和阻塞式IO的區別
爲了學習非阻塞IO用法,首先要弄清楚非阻塞和阻塞的區別。非阻塞式IO是用戶發出IO請求後不進行等待,直接獲得一個結果,通常使用時用O_NONBLOCK配合fcntl來完成。阻塞式IO是當用戶線程發出IO請求之後,內核會去查看數據是否就緒,如果沒有就緒就會等待數據就緒,而用戶線程就會處於阻塞狀態,用戶線程交出CPU,常見的阻塞有wait、pause、sleep等函數,read或write某些文件時。
2. 阻塞式IO的好處
對於內核來說,內部大部分默認的IO方式都設置爲了阻塞式,這樣的好處是爲了充分發揮操作系統的性能,讓CPU時刻工作在被需要的情況。比如對於A進程來說,它需要滿足一定的條件才能繼續往後進行,但是可能在短時間內該條件不能夠滿足,那麼該進程會阻塞住,並交出CPU供其他進程使用。等到條件滿足時,阻塞的地方解除阻塞,CPU回到該進程繼續執行。這樣極大程度地提高了CPU的利用率,減少原地踏步的時間,提高了整體系統的效率。
3. 阻塞式IO的困境
但是對於一個進程來說,裏面可能有2個阻塞式IO的地方,這就面臨着一個問題:先阻塞的地方需要滿足條件後才能去執行後阻塞的地方,也就是如果後阻塞的地方雖然達到了條件,但是先阻塞的地方卡住了,後面的結果還是沒法得到。
舉個例子:設置read函數來讀取鼠標和鍵盤輸入的內容,先對鼠標進行阻塞式訪問,再對鍵盤進行阻塞式訪問,此時先晃動鼠標得到鼠標的內容,再鍵盤輸入得到鍵盤的內容。但是如果先鍵盤輸入,那麼進程會一直阻塞在鼠標輸入那裏,直到晃動鼠標才能夠通過,這就帶來了一個輸入必須有先後順序的困擾。
二、解決阻塞式IO的困境
1. 非阻塞式IO方式
最簡單的解決方法就是將2個IO位置改變爲非阻塞的方式,類似於一種輪詢的方式,通過循環讀取鼠標和鍵盤來執行對應的IO操作。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
// 讀取鼠標
int fd = -1;
int flag = -1;
char buf[200];
int ret = -1;
fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
perror("open:");
return -1;
}
// 把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(buf, 0, sizeof(buf));
//printf("before 鍵盤 read.\n");
ret = read(0, buf, 5);
if (ret > 0)
{
printf("鍵盤讀出的內容是:[%s].\n", buf);
}
}
return 0;
}
2. IO多路複用的方式
IO多路複用的方式通常需要藉助select或poll函數,表現形式爲外部阻塞式,內部非阻塞式自動輪詢多路阻塞式IO。
外部阻塞式的意思是select/poll函數對外表現爲阻塞式,也就是最普通的阻塞式方式,兩個IO都被封裝在了select/poll中。內部非阻塞式自動輪詢的意思是,在封裝的內部,對於鼠標和鍵盤這兩個輸入一直處於自動輪詢的方式,誰滿足條件誰輸出。多路阻塞式IO的意思是鼠標和鍵盤的封裝內部仍然爲阻塞式IO。
那麼內部仍然是阻塞式IO的話跟之前不久一樣了嗎,還是會卡住?答案當然不是了,對於是否滿足輸出條件已經在最外層的select/poll中進行判斷了,所以內部IO雖然還是阻塞式的,但是如果判斷進來以後說明條件已經滿足,即雖然是阻塞式,但是一定會執行。在select內部封裝的兩個IO相當於並行的,不存在先後順序,只要滿足條件就會到對應的分支去執行對應的操作。
(1)select函數
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
int main(void)
{
// 讀取鼠標
int fd = -1, ret = -1;
char buf[200];
//定義一個文件描述符集
fd_set myset;
//定義溢出時間的結構體
struct timeval tm;
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
// 當前有2個fd,一共是fd一個是0
// 處理myset
FD_ZERO(&myset); //先清零
FD_SET(fd, &myset); //綁定鼠標
FD_SET(0, &myset); //綁定鍵盤
tm.tv_sec = 10; //設置最大等待時間爲10s
tm.tv_usec = 0;
// int select(int nfds, fd_set *readfds, fd_set *writefds,
// fd_set *exceptfds, struct timeval *timeout);
ret = select(fd+1, &myset, NULL, NULL, &tm);
if (ret < 0)
{
perror("select: ");
return -1;
}
else if (ret == 0)
{
printf("超時了\n");
}
else
{
// 等到了一路IO,然後去監測到底是哪個IO到了,處理之
if (FD_ISSET(0, &myset))
{
// 這裏處理鍵盤
memset(buf, 0, sizeof(buf));
read(0, buf, 5);
printf("鍵盤讀出的內容是:[%s].\n", buf);
}
if (FD_ISSET(fd, &myset))
{
// 這裏處理鼠標
memset(buf, 0, sizeof(buf));
read(fd, buf, 50);
printf("鼠標讀出的內容是:[%s].\n", buf);
}
}
return 0;
}
(2)poll函數
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
int main(void)
{
// 讀取鼠標
int fd = -1, ret = -1;
char buf[200];
struct pollfd myfds[2] = {0};
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
// 初始化我們的pollfd
myfds[0].fd = 0; // 鍵盤
myfds[0].events = POLLIN; // 等待讀操作
myfds[1].fd = fd; // 鼠標
myfds[1].events = POLLIN; // 等待讀操作
ret = poll(myfds, fd+1, 10000);
if (ret < 0)
{
perror("poll: ");
return -1;
}
else if (ret == 0)
{
printf("超時了\n");
}
else
{
// 等到了一路IO,然後去監測到底是哪個IO到了,處理之
if (myfds[0].events == myfds[0].revents)
{
// 這裏處理鍵盤
memset(buf, 0, sizeof(buf));
read(0, buf, 5);
printf("鍵盤讀出的內容是:[%s].\n", buf);
}
if (myfds[1].events == myfds[1].revents)
{
// 這裏處理鼠標
memset(buf, 0, sizeof(buf));
read(fd, buf, 50);
printf("鼠標讀出的內容是:[%s].\n", buf);
}
}
return 0;
}
3. 異步IO的方式
異步IO可以理解爲操作系統用軟件實現的一套中斷響應系統。
它的工作方式爲:我們當前進程註冊一個異步IO事件(使用signal註冊一個信號SIGIO的處理函數),然後當前進程可以正常處理自己的事情,當異步事件發生後當前進程會收到一個SIGIO信號從而執行綁定的處理函數去處理這個異步事件。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int mousefd = -1;
// 綁定到SIGIO信號,在函數內處理異步通知事件
void func(int sig)
{
char buf[200] = {0};
if (sig != SIGIO)
return;
read(mousefd, buf, 50);
printf("鼠標讀出的內容是:[%s].\n", buf);
}
int main(void)
{
// 讀取鼠標
char buf[200];
int flag = -1;
mousefd = open("/dev/input/mouse0", O_RDONLY);
if (mousefd < 0)
{
perror("open:");
return -1;
}
// 把鼠標的文件描述符設置爲可以接受異步IO
flag = fcntl(mousefd, F_GETFL);
flag |= O_ASYNC;
fcntl(mousefd, F_SETFL, flag);
// 把異步IO事件的接收進程設置爲當前進程
fcntl(mousefd, F_SETOWN, getpid());
// 註冊當前進程的SIGIO信號捕獲函數
signal(SIGIO, func);
// 讀鍵盤
while (1)
{
memset(buf, 0, sizeof(buf));
//printf("before 鍵盤 read.\n");
read(0, buf, 5);
printf("鍵盤讀出的內容是:[%s].\n", buf);
}
return 0;
}