Linux應用編程和網絡編程(8)-------Linux中的高級IO


一,非阻塞IO與阻塞式IO

1. 非阻塞式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的地方,這就面臨着一個問題先阻塞的地方需要滿足條件後才能去執行後阻塞的地方,也就是如果後阻塞的地方雖然達到了條件,但是先阻塞的地方卡住了,後面的結果還是沒法得到

4.舉個例子:設置read函數來讀取鼠標和鍵盤輸入的內容,先對鼠標進行阻塞式訪問,再對鍵盤進行阻塞式訪問,此時先晃動鼠標得到鼠標的內容,再鍵盤輸入得到鍵盤的內容。但是如果先鍵盤輸入,那麼進程會一直阻塞在鼠標輸入那裏,直到晃動鼠標才能夠通過,這就帶來了一個輸入必須有先後順序的困擾

#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;
	char buf[200];
	
	fd = open("/dev/input/mouse1", O_RDONLY);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	
	memset(buf, 0, sizeof(buf));
	printf("before 鼠標 read.\n");
	read(fd, buf, 50);
	printf("鼠標讀出的內容是:[%s].\n", buf);
	
	
	// 讀鍵盤
	memset(buf, 0, sizeof(buf));
	printf("before 鍵盤 read.\n");
	read(0, buf, 5);
	printf("鍵盤讀出的內容是:[%s].\n", buf);
	
	
	return 0;
}

:鍵盤的標準輸入設備,對應的文件描述符是0;鼠標不是標準設備,但是可以ls /dev/input查看,確認當前使用的設備後,open("/dev/input/mouse1", O_RDONLY);打開對應的設備文件即可。
在這裏插入圖片描述


二、併發式阻塞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/mouse1", 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));
		ret = read(fd, buf, 50);
		if (ret > 0)
		{
			printf("鼠標讀出的內容是:[%s].\n", buf);
		}
		
		// 讀鍵盤
		memset(buf, 0, sizeof(buf));
		ret = read(0, buf, 5);
		if (ret > 0)
		{
			printf("鍵盤讀出的內容是:[%s].\n", buf);
		}
	}
	
	return 0;
}

實驗現象分析:無論是先動鼠標還是先動鍵盤,都能打印信息,但是CPU被一直耗在這裏,其他進程不能得到CPU調度,降低了CPU的使用率。

在這裏插入圖片描述


2、IO多路複用


2.1、IO多路複用的原理

IO多路複用的方式通常需要藉助select或poll函數,表現形式爲外部阻塞式,內部非阻塞式自動輪詢多路阻塞式IO
外部阻塞式的意思是select/poll函數對外表現爲阻塞式,也就是最普通的阻塞式方式,兩個IO都被封裝在了select/poll中。內部非阻塞式自動輪詢的意思是,在封裝的內部,對於鼠標和鍵盤這兩個輸入一直處於自動輪詢的方式,誰滿足條件誰輸出。多路阻塞式IO的意思是鼠標和鍵盤的封裝內部仍然爲阻塞式IO。

那麼內部仍然是阻塞式IO的話跟之前不久一樣了嗎,還是會卡住?答案當然不是了,對於是否滿足輸出條件已經在最外層的select/poll中進行判斷了,所以內部IO雖然還是阻塞式的,但是如果判斷進來以後說明條件已經滿足,即雖然是阻塞式,但是一定會執行。在select內部封裝的兩個IO相當於並行的,不存在先後順序,只要滿足條件就會到對應的分支去執行對應的操作。

2.2、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/mouse1", 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;
	tm.tv_usec = 0;

	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/mouse1", 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


1、何爲異步IO
異步IO可以理解爲操作系統用軟件實現的一套中斷響應系統
它的工作方式爲:我們當前進程註冊一個異步IO事件(使用signal註冊一個信號SIGIO的處理函數),然後當前進程可以正常處理自己的事情,當異步事件發生後當前進程會收到一個SIGIO信號從而執行綁定的處理函數去處理這個異步事件

2、涉及的函數:
(1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN)
(2)signal或者sigaction(SIGIO)

3、代碼實踐

#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/mouse1", 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));
		read(0, buf, 5);
		printf("鍵盤讀出的內容是:[%s].\n", buf);
	}
		
	return 0;
}

4、存儲映射IO


1、mmap函數
mmap是memory map的縮寫,意思是存儲映射。其實就會將普通文件的硬盤空間的物理地址映射到進程空間的虛擬地址。通常情況下,進程空間的虛擬地址只映射自己底層物理空間的物理地址,但是使用mmap時,他會將文件的硬盤空間的地址也映射到虛擬地址空間,這麼一來應用程序就可以直接通過映射的虛擬地址操作文件,根本就不需要read、write函數了,使用地址操作時省去了繁雜的中間調用過程,可以快速對文件進行大量數據的輸入輸出。

2、IPC之共享內存

存儲映射與共享內存在原理上很像,都是進程虛擬內存空間向外做映射。
共享內存原理:共享內存是讓不同的進程空間映射到同一片物理內存上,然後通過共享的物理內存來實現進程間通信

3.對比存儲映射和共享內存

(1)存儲映射,其實也可以用來實現進程間通信
比如A和B進程都映射到同一個普通文件上,這時A進程往裏寫數據,B進程從裏面讀數據,反過來也是一樣的,如此就實現了進程間的通信。但是這頂多只算是廣義上的通信,所謂廣義上的通信就是,只要不是OS提供專門的IPC,就不是專門的進程間通信,只能算是廣義的IPC。實際上,我們也不會使用mmap映射普通文件來實現進程間通信,因爲操作硬盤的速度相比操作內存來說低了很多,如果你想實現進程間大量數據通信的話,完全可以使用與存儲映射原理類似的“共享內存”來實現,而且速度很快。

(2)雖然存儲映射和共享內存原理相似,但是各自用途不同
共享內存  實現進程間大量數據通信(共享)
存儲映射  對文件進行大量數的高效輸入輸出

4、存儲映射IO的特點
(1)共享而不是複製,減少內存操作
(2)處理大文件時效率高,小文件不划算

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