(九)高級IO

目錄

1.非阻塞IO

1.1.阻塞讀文件

 1.2.如何實現非阻塞讀

(1)打開文件時指定O_NONBLOCK狀態標誌

(2)通過fcntl函數指定O_NONBLOCK來實現 

1.3.實現同時“讀鼠標”和“讀鍵盤”

2.文件鎖

2.1.文件鎖的作用

2.2.多進程讀寫文件

2.3.文件鎖

2.3.1.文件鎖的讀鎖與寫鎖

2.3.2.使用文件鎖對文件進行保護

2.3.3.文件鎖的加鎖方式

2.4.文件鎖的實現

2.4.1.fcntl的函數實現

 2.4.2.文件鎖的原理

2.4.3.文件鎖其它值得注意的地方

2.4.4.使用flock函數來實現文件鎖

3.io多路複用

3.1.有關多路IO

(1)多路IO的工作原理

(2)多路IO有什麼優勢

(3)select和poll

3.2.多路io之select機制

3.3.多路io 之 poll機制

4.異步io

4.1.回顧同時讀鍵盤、鼠標的方法

4.2.異步IO的原理

4.3.使用異步IO方式讀鼠標和鍵盤

4.4.使用異步IO時,應用層的設置步驟

5.存儲映射

5.1.普通讀寫文件方式的缺點

5.1.1.普通讀寫文件的特點

5.1.2.普通讀寫方式的缺點:面對大量數據時顯得很喫力

5.2.存儲映射所用的mmap函數

5.2.1.mmap(memory map)的原理

5.2.2.映射時,具體映射到了進程空間的什麼位置呢?

5.2.3.對比IPC之共享內存

5.2.4.mmap函數

5.2.5.munmap


1.非阻塞IO

1.1.阻塞讀文件

讀某些文件時,如果文件沒有數據的話,往往會導致讀操作會阻塞(休眠)。

(1)讀鼠標、鍵盤等字符設備文件
(2)讀管道文件(有名無名)


(1)疑問:讀普通文件會阻塞嗎?

            讀普通文件時,如果讀到了數據就成功返回,如果沒有讀到數據返回0,總之不會阻塞。

(2)疑問:寫文件時會阻塞嗎?

            在寫某些文件時,當文件不能立即接收寫入的數據時,也可能會導致寫操作阻塞,一直阻塞到寫成功爲止。

 1.2.如何實現非阻塞讀

(1)打開文件時指定O_NONBLOCK狀態標誌

 fd = open("/dev/input/mouse0", O_RDONLY|O_NONBLOCK);

(2)通過fcntl函數指定O_NONBLOCK來實現 

什麼情況下會使用fcntl來實現

1)情況1:當文件已經被open打開了,但是open是並沒有指定你要的文件狀態標誌

2)情況2:沒辦法在open指定,你手裏只有一個文件描述符fd,此時就使用fcntl來重設或者補設

 

當然我們使用fcntl不僅僅只能重設或者補設O_NONBLOCK,也可以重設或者補設O_TRUNC/O_APPEND等任何你需要的“文件狀態”標誌。

 

例子:將0設置爲O_NONBLOCK

//· 重設
	fcntl(0, F_SETFL, O_RDONLY|O_NONBLOCK);

//· 補設
	flag = fcntl(0, F_GETFL); 	      //獲取原有文件狀態標誌
	flag = flag | O_NONBLOCK; 	     //通過|操作,在已有的標誌上增設O_NONBLOCK
	fcntl(0, F_SETFL, flag); 		//將修改後的“文件狀態標誌”設置回去
	

 

1.3.實現同時“讀鼠標”和“讀鍵盤”

(1)fork子進程,然後父子進程兩線任務

(2)創建次線程,主線程和次線程兩線任務

(3)將鼠標和鍵盤設置爲“非阻塞”,while輪詢的讀。

2.文件鎖

2.1.文件鎖的作用

顧名思義,就是用來保護文件數據的。

當多個進程共享讀寫同一個文件時,爲了不讓進程們各自讀寫數據時相互干擾,我們可以使用進程信號量來互斥實現,除了可以使用進程信號量以外,還可以使用“文件鎖”來實現,而且功能更豐富,使用起來相對還更容易些。

2.2.多進程讀寫文件

                           

多進程共享讀寫同一個文件時,如果數據很重要的話,爲了防止數據相互修改,應該滿足如下讀寫條件:

(1)寫與寫應該互斥

(2)讀與寫也應該是互斥的

            1)某個進程正在寫數據,而且在數據沒有寫完時,其它進程不能讀數據

            2)某個進程正在讀數據,在數據沒有讀完之前,其它進程不能寫數據

(3)讀與讀共享

2.3.文件鎖

2.3.1.文件鎖的讀鎖與寫鎖

讀鎖、寫鎖之間關係

(1)讀鎖和讀鎖共享:可以重複加讀鎖,別人加了讀鎖在沒有解鎖之前,我依然可以加讀鎖,這就是共享。    

(2)讀鎖與寫鎖互斥:別人加了讀鎖沒有解鎖前,加寫鎖會失敗,反過來也是如此。

              加鎖失敗後兩種處理方式

                       - 阻塞,直到別人解鎖然後加鎖成功爲止
                       - 出錯返回,不阻塞

(3)寫鎖與寫鎖互斥:別人加了寫鎖在沒有解鎖前,不能加寫鎖,加寫鎖會失敗。

2.3.2.使用文件鎖對文件進行保護

讀文件時加讀鎖,寫文件時就加寫鎖,然後就可以很容易的實現符合如下要求的資源保護。

1)寫與寫之間互斥
2)讀與寫之間互斥
3)讀與讀之間共享

2.3.3.文件鎖的加鎖方式

(1)對整個文件內容加鎖

(2)對文件某部分內容加鎖    

2.4.文件鎖的實現

實現文件鎖時,我們還是需要使用fcntl函數。

2.4.1.fcntl的函數實現

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, .../*struct flock *flockptr */ );

(1)功能

  • fcntl函數有多種功能,這裏主要介紹實現文件鎖的功能,
  • 當cmd被設置的是與文件鎖相關的宏時,fcntl就是用於實現文件鎖。

(2)返回值:成功返回0,失敗則返回-1,並且errno被設置。        

(3)參數

         

struct flock結構體

struct flock
{
		short l_type;   // Type of lock: F_RDLCK,F_WRLCK, F_UNLCK 
		short l_whence; //How to interpret l_start:SEEK_SET, SEEK_CUR, SEEK_END
		off_t l_start;   // Starting offset for lock 
		off_t l_len;    //Number of bytes to lock 
		pid_t l_pid;    //PID of process blocking our lock(F_GETLK only) 
}

成員說明:

 

 2.4.2.文件鎖的原理

鏈表上節點代表是一把鎖(讀鎖和寫鎖),節點存在時表示沒有解鎖,如果解鎖了鎖節點就不存在了

鎖節點記錄了鎖的基本信息。

    · 鎖類型
    · 加鎖的起始位置(l_whence、l_start)
    · 加鎖的長度(l_len)
    · 當前正在加着鎖的那個進程的PID


加鎖時,進程會檢查共享的文件鎖鏈表。

(1)進程想加讀鎖

1)如果鏈表上只有讀鎖節點

  表明目前其它進程對該文件只加了讀鎖,由於讀鎖共享,所以不管鏈表上有幾個讀鎖節點,當前進程都能成功加讀鎖。

2)如果鏈表上有一個寫鎖節點

表明目前有進程對文件加了寫鎖,鎖節點還存在,表示人家目前還沒有解鎖,讀鎖和寫鎖是互斥的,所以當前不能加讀鎖,別人解鎖後才能加讀鎖,加鎖後鏈表上就插入一個讀鎖節點。

(2)你想加寫鎖

1)如果鏈表上有讀鎖節點,別人還沒有解鎖,讀鎖與寫鎖互斥,不能加寫鎖。
        
2)如果鏈表上有寫鎖節點,別人還沒有解鎖,寫鎖與寫鎖互斥,多以當前進程不能加寫鎖

(3)對比進程信號量

1)進程信號量:進程間共享信號量集合,通過檢查集合中信號量的值,從而知道自己能不能操作
2)文件鎖:      進程共享文件鎖鏈表,通過檢查鏈表上的鎖節點,從而知道自己能不能操作

2.4.3.文件鎖其它值得注意的地方

(a)在同一進程中,如果多個文件描述符指向同一文件,只要關閉其中任何一個文件描述符

         那麼該進程加在文件上的所有文件鎖將會被刪除,也就是該進程在“文件鎖鏈表”上的“讀鎖寫鎖”節點會被刪除。、

         進程終止時會關閉所有打開的文件描述符,所以進程結束時會自動刪除所有加的文件鎖。

(b)父進程所加的文件鎖,子進程不會繼承

多線程間能不能使用fcntl實現的文件鎖呢?

可以,但是線程不能使用同一個open返回的文件描述符,線程必須使用自己open所得到的文件描述符纔有效。

2.4.4.使用flock函數來實現文件鎖

flock與fcntl所實現的文件鎖一樣,既能夠用在多進程上,也能用在多線程上,而且使用起來比fcntl的實現

#include<sys/file.h>
	
int flock(int fd, int operation);

1)用於多進程

      flock用於多進程時,各進程必須獨立open打開文件

      需要注意的是親緣進程(父子進程),子進程不能使用從父進程繼承而來的文件描述符,

      父子進程flock時必須使用獨自open所返回的文件描述符。

      這一點與fcntl實現的文件鎖不一樣,父子進程可以使用各自open返回的文件描述符加鎖,、

      但是同時子 進程也可以使用從父進程繼承而來的文件描述符加鎖。

                                                 

2)用於多線程

      用於多線程時與用於多進程一樣,各線程必須使用各自open所返回的文件描述符才能加鎖。

3.io多路複用

3.1.有關多路IO

(1)多路IO的工作原理

我們以阻塞讀爲例

                   

如果是阻塞寫的話,需要將文件描述符加入寫集合,不過我們說過對於99%的情況來說,寫操作不會阻塞
所以一般情況下對於寫來說,使用多路Io沒有意義。

注意:對於多路io來說,只有操作阻塞的fd纔有意義,如果文件描述符不是阻塞的,使用多路IO沒有意義。

(2)多路IO有什麼優勢

比如以同時讀寫鼠標、讀鍵盤爲例,如果使用,

(3)select和poll

多路IO有兩種實現方式,分別是poll和select,其中select會比poll更常用些。

3.2.多路io之select機制

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
						struct timeval *timeout);

1)功能

      

2)參數

            

struct timeval 
{
	long    tv_sec;  /* seconds(秒) */
	long    tv_usec; /* microseconds (微秒)*/
};
//· tv_sec:設置秒
//· tv_usec:設置微妙
//時間精度爲微妙,也就是說可以設置一個精度爲微妙級別的超時時間。

3)返回值

   

    select每次重新監聽時需要重新設置“集合”和“超時時間”,因爲每次select監聽結束時會清空“集合”和“超時時間”。

3.3.多路io 之 poll機制

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

功能

  

參數

  

   poll監聽時如果沒有動靜就阻塞,有動靜就不再阻塞,返回有動靜的fd的數量。

   如何知道是那些fd有動靜?

         如果文件描述符“發生的事件”==“實際事件”,就說明希望的事件來了,就是對fd進行相應的“讀或寫”操作

         

返回值:

        

4.異步io

4.1.回顧同時讀鍵盤、鼠標的方法

  • 1)多進程
  • 2)多線程
  • 3)將“讀鼠標”和“讀鍵盤”設置爲非阻塞
  • 4)多路IO(select、poll機制)
  • 5)異步IO
     

4.2.異步IO的原理

前面四種方式都是主動的去讀,對於read函數來說它並不知道是不是一定有數據,如果有數據就讀到數據,沒有數據要麼阻塞直到讀到數據爲止,要麼就不阻塞。

 

異步IO的原理就是,底層把數據準備好後,內核就會給進程發送一個“異步通知的信號”通知進程,表示數據準備好了,然後調用信號處理函數去讀數據,在沒有準備好時,進程忙自己的事情。

4.3.使用異步IO方式讀鼠標和鍵盤

進程正常阻塞讀鍵盤,然後將讀鼠標設置爲異步IO方式。

進程正常阻塞讀鍵盤時,如果鼠標沒有數據的話,進程不關心讀鼠標的事情,如果鼠標數據來了,底層鼠標驅動就會向進程發送一個SIGIO信號,然後調用註冊的SIGIO信號捕獲函數讀鼠標數據。

當然也可以反過來,進程正常阻塞讀鼠標,然後將讀鍵盤設置爲異步IO方式。

不過使用異步IO有兩個前提

(1)底層驅動必須要有相應的發送SIGIO信號的代碼,只有這樣當底層數據準備好後,底層纔會發送SIGIO信號給進程。

(2)應用層必須進行相應的異步IO的設置,否者無法使用異步IO

                   應用層進行異步IO設置時,使用的也是fcntl函數。

4.4.使用異步IO時,應用層的設置步驟

5.存儲映射

5.1.普通讀寫文件方式的缺點

5.1.1.普通讀寫文件的特點

使用文件IO的read/write來進行文件的普通讀寫時,函數經過層層的調用後,才能夠最終操作到文件,    

                 

從應用緩存到文件,效率很低

(1)cpu執行一堆的函數,很耗費cpu資源,而且浪費時間
(2)中間一堆的緩存都是函數從內存開闢的,浪費內存資源,而且數據在各緩存間倒騰時也很耗費時間

5.1.2.普通讀寫方式的缺點:面對大量數據時顯得很喫力

5.2.存儲映射所用的mmap函數

5.2.1.mmap(memory map)的原理

mmap的原理就是,既然直接read、write很費勁,那我乾脆拋棄read、write的操作,mmap採用直接映射的方式實現

mmap映射時,比如映射普通文件,其實就會將普通文件的硬盤空間的物理地址映射到進程空間的虛擬地址。 

通常情況下,進程空間的虛擬地址只映射自己底層物理空間的物理地址,但是使用mmap時,他會將文件的硬盤空間的地址也映射到虛擬地址空間

這麼一來應用程序就可以直接通過映射的虛擬地址操作文件,根本就不需要read、write函數了,使用地址操作時省去了繁雜的中間調用過程,可以快速對文件進行大量數據的輸入輸出。

 

疑問:使用存儲映射時,read、write被省掉了,open是不是也被省掉了?

    答:open不能省,必須要將文件open後,才能使用mmap進行映射。

5.2.2.映射時,具體映射到了進程空間的什麼位置呢?

映射到了“進程應用空間”堆和棧中間那片虛擬地址的位置。

            

(1)進程內核空間:用於映射“OS”所在的物理內存空間
(2)進程應用空間:用於映射“應用程序”所在的物理內存空間
 

5.2.3.對比IPC之共享內存

1)存儲映射,其實也可以用來實現進程間通信

2)雖然存儲映射和共享內存原理相似,但是各自用途不同

      

5.2.4.mmap函數

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

5.2.5.munmap

#include <sys/mman.h>

int munmap(void *addr, size_t length);

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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