Linux-應用編程-學習筆記(18):對於阻塞式IO困境的解決

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;
}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章