IO分兩步
- 等待
- 數據拷貝
五種IO模型
阻塞IO
在內核將數據準備好之前,系統調用會一直等待。 所有的套接字,默認都是阻塞方式。
非阻塞IO
如果內核還未將數據準備好,系統調用會直接返回,並且返回EWOULDBLOCK錯誤碼。非阻塞IO往往需要程序員循環的方式反覆嘗試讀寫文件描述符,這個過程稱爲輪詢。 這對CPU來說是較大的浪費,一 般只有特定場景下才使用。
信號驅動IO
內核將數據準備好的時候,使用SIGIO信號通知應用程序進行IO操作。
異步IO
由內核在數據拷貝完成時,通知應用程序。
同步: 爲了完成功能發起調用,若當前不具備完成條件,則自己等待完成功能後返回。
異步: 爲了完成功能發起調用,但功能由別人來完成。
同步與異步的區別: 功能是否由別人來完成。
同步通常都是阻塞操作
異步阻塞操作: 等待別人完成操作。
異步非阻塞操作: 不等待別人完成操作 。
同步與異步的優缺點: 同步流程控制簡單但是效率較低,異步流程控制較難,但是效率相對較高。
多路轉接IO/多路複用IO
是一種IO事件的監控,同時對大量的描述符進行事件監控,監控描述符是否具備IO條件。雖然從流程圖上看起來和阻塞IO類似,實際上最核心在於IO多路轉接能夠同時等待多個文件描述符的就緒狀態。
IO多路轉接模型
都是實現對大量描述符進行事件(描述符的可讀/可寫/異常)默認阻塞監控的操作,當大量的描述符中有就緒時返回。
就緒
對可讀事件來說,緩衝區中有數據就是讀就緒。
對於可寫事件來說,緩衝區中有空閒空間就是寫就緒。
作用
TCP服務端程序,同一時間只能與一個客戶端進行通信一次。原因就是服務端不知道客戶端什麼 時候有數據到來,只能實現一個死流程,這個死流程就會導致程序阻塞。但是當用戶知道那個描述符的數據什麼時候到來,然後再去處理,流程變成了活動的流程(只針對那些就緒描述符進行處理)靈活性增強,程序將不再阻塞。
select模型
通過對幾個事件集合中的描述進行各自的事件監控,當對應集合中有描述符事件就緒則返回(返回之前將集合中沒有就緒的描述符全部移除)。
select模型中的一些操作
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds(maxfds) //監控的描述符中,最大的那個描述符+1,否則會遍歷所有描述符
readfds //讀時間集合
writefds //寫事件集合
exceptfds //異常事件集合
timeout //select等待超時事件
fd_set //描述符集合------是一個位圖----位圖大小取決於__FD_SETSIZE = 1024
void FD_CLR(int fd, fd_set* set); //將指定的描述符從集合中移除
int FD_ISSET(int fd, fd_set* set); //判斷指定的描述符是否在集合中
void FD_SET(int fd, fd_set* set); //將指定的描述符添加到監控集合中
void FD_ZERO(fd_set* set); //清空描述符集合
流程
- 針對描述符不同的事件定義不同事件描述符集合fd_set(位圖)(可讀/可寫/異常)。
- 將指定的描述符按照不同的所關心的事件添加到不同的集合中。
- 將集合拷貝到內核進行監控,監控的原理是對所有描述符的輪詢遍歷。
- 當有描述符就緒時,再調用返回之前,將集合中沒有就緒的描述符剔除出去。
- 用戶操作,對所有的描述符進行遍歷,看哪一個描述符還在集合中,則這個描述符已經就緒。
select的優缺點分析
優點
- 遵循posix標準,可以跨平臺,移植性好。
- select監控的超時事件更加精細(微秒)。
缺點
- select所能監控的描述符是有上限的–默認1024,取決於__FD_SETSIZE。
- select實現監控的原理是在內核中進行輪詢遍歷狀態,因此性能會隨着描述符增多而下降。
- select返回值每次監控都會修改監控集合(剔除未就緒描述符),需要用戶每次監控時重新添加描述符到集合中。
- select要監控的集合中的描述符數據,需要每次都重新向內核中拷貝。
- select不會告訴用戶具體哪一個描述符就緒,需要用戶遍歷所有描述符進行查找,性能隨着描述符增多而降低,而且增加了代碼複雜度。
poll模型
監控實現原理
採用一個描述符事件結構的方式對描述符所關心的事件進行監控。
// pollfd結構
struct pollfd
{
int fd; /* file descriptor */ //用戶監控的文件描述符
short events; /* requested events */ //保存用戶關心的事件
short revents; /* returned events */ //保存當前就緒的事件
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds //事件數組
nfds //監控的事件個數
timeout //超時等待事件
流程
- 用戶定義事件數組,對描述符可以添加關心事件,進行監控。
- poll實現監控的原理也是將數據拷貝到內核,然後進行輪詢遍歷監控,性能隨着描述符的增多而下降。
- 若有事件就緒,則修改這個響應描述符事件結構中的實際就緒事件。
- 用戶根據返回的revents判斷哪一個事件就緒,然後進行操作即可。
- poll也不會直接告訴用戶哪一個描述符事件就緒,只是告訴了用戶有就緒事件,需要用戶對事件結構數組進行遍歷遍歷查詢,查看實際發生的事件是否是自己關心的事件,來查找就緒,進而進行操作。
poll的優缺點分析
優點
- 採用事件結構的方式對描述符進行監控,簡化了多個事件集合的監控方式
- 沒有描述符的具體監控上限
缺點
- 不能跨平臺
- poll採用輪詢遍歷方式判斷就緒,性能隨着描述符增多而性能下降
- poll也不會告訴用戶具體那個描述符事件就緒需要用戶遍歷所有描述符進行查找,性能隨着描述符增多而降低,而且增加了代碼複雜度
epoll模型
Linux下性能最高的IO多路轉接模型
事件就緒
- 可讀事件就緒: 接收緩衝區中的數據大小大於低水位標記(18),就會觸發可讀事件。
- 可寫事件就緒: 發送緩衝區中的空閒空間大小大於低水位標記(18),就會觸發可寫事件。
int epoll_create(int size); //創建一個epoll的句柄
//功能:在內核中創建一個結構體, struct eventpoll rbr--紅黑樹 rdlist--雙向鏈表
size:能監控描述符的上限,自從linux2.6.8內核之後,size參數是被忽略的,只要》0就可以
返回值:文件描述符(非負整數) epoll的操作句柄
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//向內核中epfd對應的epoll結構的eventpoll結構中添加/移除/修改所監控的事件結構
epfd:epollcreat返回的epoll操作句柄
op 用戶要進行的操作
EPOLL_CTL_ADD :註冊新的fd到epfd中;向內核的eventpoll中添加要監控的事件結構
EPOLL_CTL_MOD :修改已經註冊的fd的監聽事件;修改內核中所監控的事件結構
EPOLL_CTL_DEL :從epfd中刪除一個fd;從內核的eventpoll中移除要監控的事件結構
fd 用戶所要監控的描述符
event 描述符對應所要監控的事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//功能:開始監控
epfd:epoll操作句柄
events:事件結構體數組,用於保存就緒的描述符對應事件
maxevents:用於確定一次最多獲取的就緒事件個數(防止events數組溢出)
timeout:超時等待事件設置---毫秒
返回值: <0 出錯 ==0 超時等待 >0 就緒的事件個數
eventpoll
{
rbr--紅黑樹--保存用戶添加的事件結構節點
rdlist--雙向鏈表 描述符就緒時,回調函數自動將對應的事件結構地址添加到雙向鏈表中
}
epoll_wait
- 告訴內核要開始對描述符進行監控。
- 操作系統對描述符進行監控(採用的是事件觸發方式進行監控,爲每一個要監控的描述符都定義了一個事件,並且對這個事件定義一個事件回調函數)。
- 這個事件回調函數做的事就是將就緒的這個描述符所對應的epoll_event事件結構添加到雙向鏈表rdlist中。
- epoll_wait並沒有立即返回,而是每隔一會就看一下,內核中的eventpoll中雙向鏈表(保存的都是就緒的描述符對應的事件結構)是否爲空,進而判斷是否有描述符就緒。
- 若鏈表不爲空,表示有描述符就緒,而epoll_wait即將返回,在返回之前,將就緒的描述符對應事件結構向用戶態的結構體數組(epoll_wait第二個參數)拷貝一份。
- epoll會將就緒的描述符對應事件,拷貝一份到用戶態,直接告訴用戶有哪些描述符就緒。進而用戶可以直接操作就緒的描述符。
epoll觸發方式
水平觸發
只要緩衝區中的數據大小/空閒空間大小大於低水位標記,就會觸發可讀/可寫就緒事件。
邊緣觸發
- 可讀時間: 每次只有新數據到來時纔會觸發一次可讀事件(不關注緩衝區數據有多少,要求用戶最好一次將緩衝區中的數據全部讀取)。
- 可寫事件: 每次只有緩衝區空閒空間從0變爲大於低水位標記時纔會觸發可寫事件。
通常讀寫事件混合監控的時候,對於可寫事件就會使用邊緣觸發(防止可寫事件每次在不寫入數據但是有空閒空間都會觸發事件)。
若是可讀事件被設置爲邊緣觸發,需要用戶一次性將所有數據讀取完畢,但是因爲不知道數據有多少,因此只能循環從緩衝區中讀取數據,當循環讀取但是緩衝區中沒有數據的時候,recv就會阻塞,因此邊緣觸發的可讀事件的描述符通常需要被設置爲非阻塞。
int fcnt(int fd, int cmd, .../*arg*/);
fd 文件描述符
cmd
F_GETFL 獲取描述符狀態屬性信息
F_SETFL 設置描述符狀態屬性信息
arg 用戶設置/獲取文件描述符的屬性信息
void sernonblock(int fd)
{
fcntl(fd, F_SETFL, fcntl(fd,F_GETFL,0)|O_NONBLOCK);
}
epoll的優缺點分析
優點
- epoll沒有監控的描述符上限。
- 採用事件結構簡化了select這種監控集合的監控流程。
- epoll是異步阻塞操作,發起調用讓操作系統進行描述符監控,操作系統使用的是事件回調方式對描述符進行監控,避免了select的輪詢遍歷,因此性能不會隨着描述符增多而降低。
- epoll發起調用之後進行等待–循環判斷內核中epoll就緒的事件鏈表是否爲空來確定是否有事件就緒,若有事件就緒,則將對應事件拷貝到用戶態供用戶操作。直接告訴用戶那些描述符就緒了,可以直接對描述符進行操作;沒有空遍歷,提高了性能,並且簡化了代碼流程。
- epoll描述符的事件結構只需向內核拷貝一次(內核eventpoll結構體的紅黑樹中),不需要每次拷貝。
缺點
- 不能跨平臺
- 超時等待事件只能精確到毫秒
IO多路轉接模型的適應場景
有大量的客戶端鏈接,但是同時只有少量活躍的場景。