1.fsync
應用程序通過write系統(tǒng)調用要向某個文件寫入數(shù)據(jù)的時候,內核通常是把數(shù)據(jù)寫入到內核緩沖區(qū)中,而不是直接寫到磁盤(顯式指定同步方式除外),通過這種機制,write()就可以頻繁調用并立即返回。 此時,數(shù)據(jù)未必已經(jīng)真正寫入到磁盤中,一般內核通過定時刷新的方式,把緩沖區(qū)中的數(shù)據(jù)批量寫入到文件中。 但應用程序也可以通過fsync系統(tǒng)調用來進行顯式的控制,要求某個文件對應緩沖區(qū)中的數(shù)據(jù)進行刷盤。
int fsync(int fd); //刷新某個文件描述符對應的緩沖區(qū)數(shù)據(jù)
void sync(); //不指定具體的文件描述符,刷新所有的緩沖區(qū)
2.mmap
除了標準文件操作,如read,write等,內核還提供了mmap內存映射,支持應用程序將某個文件映射到內存中(虛擬內存),既內存地址和文件數(shù)據(jù)一一對應。這樣,應用程序就可以直接通過內存來訪問文件,就像操作內存中的數(shù)據(jù)塊一樣,比如可以通過寫入內存數(shù)據(jù)區(qū),然后通過透明的映射機制就可以實現(xiàn)將文件寫入磁盤。
比標準文件讀寫效率高的原因:
標準文件操作(調用read/write等系統(tǒng)函數(shù))為了提高讀寫效率和保護磁盤,使用了頁緩存機制。這樣造成讀文件時需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內核空間,不能被用戶進程直接訪問,所以還需要將頁緩存中數(shù)據(jù)頁再次拷貝到內存對應的用戶空間中。這樣,通過了兩次數(shù)據(jù)拷貝過程,才能完成進程對文件內容的獲取任務。寫操作也是一樣,待寫入的buffer在內核空間不能直接訪問,必須要先拷貝至內核空間對應的主存,再寫回磁盤中(延遲寫回),也是需要兩次數(shù)據(jù)拷貝。
而使用mmap操作文件,創(chuàng)建新的虛擬內存區(qū)域和建立文件磁盤地址和虛擬內存區(qū)域映射這兩步,沒有任何文件拷貝操作。讀寫數(shù)據(jù)的時候,只需要從磁盤到用戶控件一次數(shù)據(jù)拷貝就可以完成,效率上更高.
使用場景:一般用于對文件讀寫性能要求較高的場景下,如rocketmq中針對commitlog文件的處理
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
參數(shù)說明:
addr 告訴內核映射文件的內存最佳地址,大部分情況下該參數(shù)傳0
prot 描述對內存區(qū)域的所請求的訪問權限,如PROT_READ表示可讀, PROT_WRITE表示可寫, PROT_EXEC表示頁可執(zhí)行,該參數(shù)不能和文件本身的訪問模式?jīng)_突,如文件本身是只讀的,就不能設置為可寫
flags 描述映射的類型和一些行為
fd 要映射的文件描述符,該參數(shù)為-1的時候,代碼的是申請一塊匿名內存映射,一般用于申請一塊大內存 |
offset 可以選擇只映射文件中的某一部分內容,offset表示開始的位移
3.select
Linux系統(tǒng)提供select函數(shù)來實現(xiàn)多路復用輸入/輸出模型,select系統(tǒng)調用是用來讓我們的應用程序監(jiān)視多個文件句柄(包括socket文件句柄)的狀態(tài)變化,程序會在select這里等待,直到被監(jiān)視的文件句柄有一個或多個發(fā)生了狀態(tài)改變(如可讀/可寫)。
尤其是針對網(wǎng)絡IO,因為如果采用單個連接提供一個線程的話,當線程數(shù)量較大時,服務端系統(tǒng)資源消耗過大,并且多個線程上下文切換導致性能降低,IO多路復用方案(如select,epoll,kqueue等)作為效率較高的替代方案。
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
參數(shù)說明:
n 一般為最大文件描述符 + 1, 既STDIN_FILENO + 1 ,該值會有l(wèi)imit限制,一般為1024,所以生產(chǎn)環(huán)境基本用epoll,kqueue等代替
readfds 監(jiān)視是否有可讀的文件描述符集合
writefds 監(jiān)視是否有可寫的文件描述符集合 | |
exceptfds 監(jiān)視是否有異常情況發(fā)生的文件描述符集合 | |
timeout 超時時間,如果在某一段時間內依然沒有相應事件觸發(fā),則會阻塞直到timeout時間過期, timeout.tv_sec單位為秒, timeout.tv_usec單位為微秒
4.epoll
由于select和poll系統(tǒng)調用存在以下幾個問題,Linux內核2.6環(huán)境新增Event Poll的方式。
select/poll每次檢查的時候是通過遍歷所有的文件描述符(fd), 尤其是對于網(wǎng)絡scoket而言,大部分存在這么一個特性,既某一個時間點里,只有很少一部分的socket是“活躍”狀態(tài),如果每次都是遍歷所有的網(wǎng)絡文件描述符的話,當文件描述符變大之后,性能就會隨著連接數(shù)變大之后線型下降
select存在最大文件描述符的限制,具體取決于常量FD_SETSIZE,不能滿足大批量的客戶端連接.(如上萬個連接的情況)
反觀epoll,則改進了以上不足的地方
1.在內核實現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實現(xiàn)的。那么,只有“活躍”的socket才會主動的去調用 callback函數(shù),其他idle狀態(tài)socket則不會
2.epoll沒有對fd描述符有限制,理論上取決于系統(tǒng)內存大小, 可以通過命令 cat /proc/sys/fs/file-max查看,大概1G內存可以創(chuàng)建10w個連接
3.epoll的具體實現(xiàn)使用mmap加速內核與用戶空間的消息傳遞,進一步提高性能
epoll實際包含3個系統(tǒng)調用組成
int epoll_create(int size);
創(chuàng)建epoll的實例,并返回epoll本身的描述符, 用于epoll_ctl和epoll_wait作為參數(shù)傳入. 參數(shù)size只要大于0即可,內核會動態(tài)獲取大小
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
添加(op=EPOLL_CTL_ADD)、修改(op=EPOLL_CTL_MOD)、刪除(op=EPOLL_CTL_DEL)要監(jiān)聽的event事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
監(jiān)視等待有IO事件發(fā)生,直到timeout過期, 類似select()調用
5.malloc
包含標準庫的方法
//在堆上申請size大小的內存
void *malloc(size_t size);
//數(shù)組形式的內存申請,且用0初始化了內存
void *calloc(size_t n, size_t size);
//重新調整已分配的內存,如果size=0則等效于free()調用
void *realloc(void *ptr, size_t size);
//釋放內存
void free(void *ptr);
//功能跟malloc一致,但返回的地址是頁對齊的
void *valloc(size_t size);
//當fd=-1的時候,表示申請匿名內存映射,一般用于申請較大內存的場景
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
//在棧中申請內存,方法執(zhí)行完之后會自動釋放,無需顯式調用free()方法
void *alloca(size_t size);
6.mlock
虛擬內存是計算機系統(tǒng)內存管理的一種技術。它使得應用程序認為它擁有連續(xù)的可用的內存(一個連續(xù)完整的地址空間,而實際上,它通常是被分隔成多個物理內存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進行數(shù)據(jù)交換。
基于某些原因如確定性和安全性,某些應用中的部分內存不允許交換到外部磁盤中,可以使用mlock(內存鎖定)來實現(xiàn)。
//鎖定內存地址不允許交換
int mlock(const void *addr, size_t len);
//某個進程想鎖定它全部的地址空間
int mlockall(int flags);
//內存解鎖,允許再次被swap到外部磁盤中
int munlock(const void *addr, size_t len);
//解鎖全部的地址空間
int munlockall();
7.sendfile
zero-copy原理,比如有如下場景,服務端響應客戶端的請求,從文件系統(tǒng)中讀取某一個文件的內容,然后通過socket方式把該內容發(fā)送給客戶端。
則具體過程可以分為以下2個步驟 (2個文件拷貝的場景同理)
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
系統(tǒng)底層執(zhí)行上面2行代碼的過程如下
1.系統(tǒng)調用 read() 產(chǎn)生一個上下文切換:從 user mode 切換到 kernel mode,然后 DMA 執(zhí)行拷貝,把文件數(shù)據(jù)從硬盤讀到一個 kernel buffer 里
數(shù)據(jù)從 kernel buffer 拷貝到 user buffer,然后系統(tǒng)調用 read() 返回,這時又產(chǎn)生一個上下文切換:從kernel mode 切換到 user mode
系統(tǒng)調用 write() 產(chǎn)生一個上下文切換:從 user mode 切換到 kernel mode,然后把步驟2讀到 user buffer 的數(shù)據(jù)拷貝到 kernel buffer(數(shù)據(jù)第2次拷貝到 kernel buffer, 不過這次是不同的 kernel buffer,這個 buffer 和 socket 相關聯(lián)
系統(tǒng)調用 write() 返回,產(chǎn)生一個上下文切換:從 kernel mode 切換到 user mode(第4次切換),然后 DMA 從 kernel buffer 拷貝數(shù)據(jù)到協(xié)議棧(第4次拷貝)
以上細節(jié)是傳統(tǒng)read/write方式進行網(wǎng)絡文件傳輸?shù)姆绞剑覀兛梢钥吹?,在這個過程當中,文件數(shù)據(jù)實際上是經(jīng)過了四次copy操作:
硬盤==>內核buf==>用戶buf==>socket相關緩沖區(qū)==>協(xié)議引擎
如果能減少切換次數(shù)和拷貝次數(shù)將會有效提升性能。linux通過系統(tǒng)調用 sendfile簡化上面步驟提升性能的,sendfile不但能減少切換次數(shù)而且還能減少拷貝次數(shù)。
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
運行流程如下:
1.sendfile系統(tǒng)調用,文件數(shù)據(jù)被copy至內核緩沖區(qū)
2.再從內核緩沖區(qū)copy至內核中socket相關的緩沖區(qū)
3.最后再socket相關的緩沖區(qū)copy到協(xié)議引擎
相較傳統(tǒng)read/write方式,2.1版本內核引進的sendfile已經(jīng)減少了內核緩沖區(qū)到user緩沖區(qū),再由user緩沖區(qū)到socket相關 緩沖區(qū)的文件copy,而在內核版本2.4之后,文件描述符結果被改變,sendfile實現(xiàn)了更簡單的方式,系統(tǒng)調用方式仍然一樣,細節(jié)與2.1版本的 不同之處在于,當文件數(shù)據(jù)被復制到內核緩沖區(qū)時,不再將所有數(shù)據(jù)copy到socket相關的緩沖區(qū),而是僅僅將記錄數(shù)據(jù)位置和長度相關的數(shù)據(jù)保存到 socket相關的緩存,而實際數(shù)據(jù)將由DMA模塊直接發(fā)送到協(xié)議引擎,再次減少了一次copy操作。