進程間通信和文件I\O

在Unix進程間通信中,大致有

1. 管道                                     pipe(),用於父子進程間通信(不考慮傳遞描述符)
2. FIFO(有名管道)       非父子進程也能使用,以文件打通
3. 文件                                     文件操作,效率可想而知
4. 本地套接字                       最穩定,也最複雜.套接字採用Unix域
5. 共享內存                            傳遞最快,消耗最小,傳遞數據過程不涉及系統調用
6. 信號                                     數據固定且短小

匿名管道(pipe)

         這種管道只能用於存在親緣關係的進程之間的通信,也就是父子進程或者是兄弟進程之間的通信。從本質上而言,匿名管道可以理解成一種特殊的文件系統,只是這種文件系統與unix下所說的文件系統不同,它不存在於磁盤上,只是存在於計算機的內存之中(和文件系統中的/proc有點類似)。其創建函數如下:

#include<unistd.h>

int pipe(int fd[2]);

管道創建成功之後通過fd返回兩個文件描述符fd[0]和fd[1]。fd[0]爲讀而打開,fd[1]爲寫而打開,fd[1]的輸出恰好是fd[0]的輸入。

由於是半雙工通信,因此通過管道進行通信的進程之間數據的流動如圖1所示,當然通過關閉不同的文件描述符,也可以使數據反向流動。


                                                                         

通過這張圖可以看出,數據是單向流動;

下面給出code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>

int main(int argc, char const *argv[])
{
	//用於管道的讀寫
	int fd[2];
	int status;
	//creat pipe
	pipe(fd);
	pid_t pid;
	pid_t ret=fork();
	char buffer[80];
	char fn[12]="i'm a child";
	if (ret==-1)
	{
		perror("fork");
		exit(1);
	}
	else if (ret==0){
		//關閉讀端
		close(fd[0]);

		write(fd[1],fn,sizeof(fn));
		//寫完之後,關閉寫端
		//close(fd[1]);
		sleep(10);
		exit(1);
	}else{
		sleep(3);
		//printf("%d%d\n",getpid(),ret);
		close(fd[1]);		//關閉寫端
		//dup2(fd[0],STDOUT_FILENO);
		status=read(fd[0],buffer,16);
		if(status==-1){
			perror("read");
		}
		else if(status==12){
			printf("read done\n");
		}
		status=read(fd[0],buffer,16);
		printf("阻塞了沒?\n");
		if(status==-1){
			perror("read");
		}
		else if(status==0){
			printf("no done\n");
		}
		printf("%s\n",buffer);	
		exit(1);
		

	}
	return 0;
}

一,首先,我讓父進程關閉寫端,子進程先關閉讀端,然後寫完一段數據後,關閉寫端,然後讓子進程等待10秒;讓父進程一直讀到沒有數據爲止,此時,從終端輸出,就可以發現,當寫端已經不存在,而讀端還在讀數據,則讀端將會讀到沒有數據的時候,就直接返回0。並不會阻塞

二,我不關閉子進程的寫端,然後讓子進程睡了10秒,讓父進程一直讀到沒有數據爲止,這個時候,因爲寫端一直存在,當讀端讀到沒有數據的時候,就會阻塞!


三,我讓子進程關閉讀端父進程同時關閉讀和寫,那麼子進程會由於在沒有讀端的情況下,覺得寫數據也沒有意思了,就會默認終止進程,發出SIGPIPE的信號,

                
//下面是父進程回收子進程時,處理信號

                wid=wait(&status);
		if(WIFEXITED(status)){
			printf("Normal termination with exit status=%d\n",
			WEXITSTATUS(status) );
		}
		if(WIFSIGNALED(status)){
			printf("Killed by signal=%d%s\n",
			WTERMSIG(status),
			WCOREDUMP(status)?"(dumped core)":"");
		}
		if(WIFSTOPPED(status)){
			printf("Stopped by signal=%d\n",
			WSTOPSIG(status));
		}

Killed by signal=13

通過查詢 kill -l ,發現13爲 SIGPIPE。

 四,如果讓父進程讀端存在,那麼一直讓子進程寫數據,則當管道滿了的時候,就會阻塞管道,等待讀端去讀數據;


下面,我簡單說一下關於共享存儲映射(存儲I/O映射)和緩衝I\O映射,由於沒有看過linux內核設計,但是我看到“普通文件IO需要複製兩次,內存映射文件mmap只需要複製一次”這句話的時候,比較迷茫,不知道它大體上是怎麼操作的;於是我翻看了書籍和一些比較好的博客,下面做一下總結:

我先把之所以迷茫的點先羅列起來,然後逐個的解釋。

一,關於內核空間和用戶空間的區別以及作用

二,頁緩存

三,普通文件I\O和映射I\O到底是怎麼運作的

  1. 關於內核空間和用戶空間的區別以及作用

(這裏,是在我看到別人總結的時候,提煉的)

首先,說到內核空間和用戶空間,就必須說到保護模式

          如果一段程序能夠完全控制物理內存,那麼它就能做到任意改變計算機的狀態,包括幹掉整個操作系統然後把自己變成操作系統;把自己變成操作系統的一部分等等。通常來說操作系統肯定是不樂意的了。

如果用戶程序自己可以訪問大部分的硬件設備;用戶程序就可以隨意修改屬於操作系統的數據。那麼隨意篡改操作系統,則會使操作系統崩潰。

單任務的情況下已經有不少問題了,到了多任務模式下,問題就更嚴重了:

  1. 因爲多個應用程序要獨立加載,如果兩個應用程序執意要使用同一個內存地址,那就會發生嚴重的問題,操作系統必須防止這種事情發生
  2. 外部設備一般來說都是很傻的,它並不知道多任務的存在,不管誰操作外部設備它都是一樣響應。這樣如果多個應用程序自己直接去操縱硬件設備,就會出現相互衝突,有可能一個程序的數據被髮送到了另一個程序等等
  3. 操作系統必須自己響應硬件中斷,通過硬件中斷來切換任務上下文,讓合適的任務在合適的時機繼續執行。如果應用程序自己把中斷響應程序改掉了,整個操作系統都會崩潰
  4. 操作系統必須有能力在單個應用程序崩潰的情況下清理這個應用程序使用的資源,保證不影響其他應用程序;這就要求它必須清楚知道每個應用程序使用了哪些資源

所以要限制應用程序的行爲,必須在應用程序和操作系統執行時有不同的狀態,核心問題在於保護關鍵寄存器和重要的物理內存。

因此必須讓CPU區分當前究竟是執行操作系統(開放所有能力)還是應用程序(限制危險功能),從而我們就將CPU執行時,劃分成兩種不同的狀態,保證在應用程序下就不會觸及操作系統的數據和代碼。從而這個時候就需要操作系統的配合,設置哪些內存可以訪問,哪些不能訪問(或者說只有操作系統狀態下能訪問),不能訪問的包括操作系統自己的代碼區和數據區、中斷向量表等。因此從這裏,我們就可以看到一點,當在內核模式下,內存還是那些內存,但此時擁有了特權,可以訪問更多的數據和代碼了;通過這樣的特權切換,可以保護硬件設備和操作系統不被破壞。但是我們需要在兩種狀態下切換,此時就需要CPU觸發中斷機制,然後進入到操作系統狀態,因此應用程序狀態不能任意切換到操作系統狀態,但也需要有觸發進入操作系統代碼並切換到操作系統狀態的能力(否則無法調用操作系統功能)

因此這裏就有了內核態用戶態;

注意到,內核態並不是一個東西,沒有處於什麼地方一說,它是CPU的兩種狀態之一。因此說切換,其實就是擁有了特權。

從而爲了判斷哪些內存能訪問,和多用戶的獨立運行,這裏就引入了虛擬地址空間,即用戶空間使用的都是虛擬地址,通過MMU單元來建立虛擬地址和物理地址的映射,而內核態下,就允許訪問所有內存。這裏注意內核態下訪問內存,並不是用戶可以直接訪問了,而是通過切換模式,進入到內核態,由操作系統代爲處理,當處理完成,則返回用戶態。

總結來自https://www.zhihu.com/question/306127044/answer/555327651

         2.頁緩存

linux中頁緩存的本質就是對於磁盤中的部分數據在內存中保留一定的副本,使得應用程序能夠快速的讀取到磁盤中相應的數據,並實現不同進程之間的數據共享。因此,linux中頁緩存的引入主要是爲了解決兩類重要的問題:

        1.磁盤讀寫速度較慢(ms 級別);

        2.實現不同進程之間或者同一進程的前後不同部分之間對於數據的共享

如果沒有進程之間的共享機制,那麼對於系統中所啓動的所有進程在打開文件的時候都要將需要的數據從磁盤加載進物理內存空間,這樣不僅造成了加載速度變慢(每次都從磁盤中讀取數據),而且造成了物理內存的浪費。爲了解決以上問題,linux操作系統使用了緩存機制.

          3.普通文件I\O和內存映射I\O

比如,我現在想從一個datd.txt中讀取數據,那麼我會調用read系統函數,接下來就是描述一下,是怎麼發生的:

        1.進程調用庫函數read()向內核發起讀文件的請求,因爲我是想從磁盤讀取,從而用戶下並沒有這個特權,切換到內核模式下,由操作系統處理

        2.內核通過檢查進程的文件描述符定位到虛擬文件系統已經打開的文件列表項,調用該文件系統對VFS的read()調用提供的接口;

        3.通過文件表項鍊接到目錄項模塊,根據傳入的文件路徑在目錄項中檢索,找到該文件的inode;

        4.inode中,通過文件內容偏移量計算出要讀取的頁;

        5.通過該inode的i_mapping指針找到對應的address_space頁緩存樹---基數樹,查找對應的頁緩存節點;

        (1)如果頁緩存節點命中,那麼直接返回文件內容;

        (2)如果頁緩存缺失,那麼產生一個缺頁異常,首先創建一個新的空的物理頁框,通過該inode找到文件中該頁的磁盤地址,讀取相應的頁填充該頁緩存(DMA的方式將數據讀取到頁緩存),更新頁表項;重新進行第5步的查找頁緩存的過程;

        6.然後將頁緩存中的數據複製到用戶空間指定的物理內存中

        7.切換到用戶模式下;


我們可以看到在一次read的讀取過程中,其發生了兩次的數據複製,一次是磁盤到頁緩存中,一次是頁緩存到用戶空間中;

(如果頁緩存已經被修改,那麼就稱它爲髒頁,那麼將由系統操作進行排序,可以進行延遲寫回磁盤,或者同步寫)

下面這兩張圖我覺得很好:


 這裏在說一下匿名管道;如果我現在創建了匿名管道,現在想將文件A的數據讀到管道中,然後將數據傳到子進程,並寫到文件B中。那麼該過程是什麼樣子的呢?

首先,進程的數據區位於0-3G的虛擬地址空間中,3G-4G爲內核區,這裏假設,文件A和文件B的部分已經存儲在頁緩存(內核緩衝區)中

(1)父進程通過系統調用read()從文件A讀取數據的過程中,父進程的狀態切換到內核態,讀取數據並保存到父進程空間中的buf中,再切換回用戶態。這裏發生了第一次數據的拷貝。
(2)父進程通過系統調用write()將讀取的數據從buf中拷貝到管道的過程中,父進程狀態切換到內核態,向管道寫入數據,再切換回用戶態。這裏發生第二次數據拷貝。
(3)子進程通過系統調用read()從管道讀取數據的過程中,子進程狀態切換到內核態,讀取數據並保存到子進程空間中的buf中,再切換回用戶態。這裏發生第三次數據拷貝。
(4)子進程通過系統調用write()將讀取的數據從buf中拷貝到文件B的過程中,子進程狀態切換到內核態,向文件B寫入數據,再切換回用戶態。這裏發生第四次數據拷貝。
    

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