文章目錄
- 創建文件描述符的函數
- pipe函數
- dup函數、dup2函數
- 讀取或寫入數據
- readv函數、writev函數
- 零拷貝
- sendfile函數
- splice函數
- tee函數
- 進程間通信——共享內存
- mmap函數 和 munmap函數
- 控制文件描述符
- fcntl函數
創建文件描述符的函數
pipe函數
不再贅述,詳情見我的另一篇博客。
值得一提的是,socket
基礎API中有一個 socketpair函數
,能夠方便地創建雙向管道:
#include<sys/types.h>
#include<sys/socket.h>
int socketpair(int domain, int type, int protocol, inf fd[2]);
// domain只能使用 UNIX 本地域協議族 AF_UNIX,因為我們僅能在本地使用這個雙向管道。
// 成功時返回0,失敗時返回-1并設置error。
dup函數、dup2函數
這兩個函數會在 CGI服務器
中用到。CGI服務器: 主要是通過把服務器本地標準輸入、輸出或者文件重定向到網絡連接中,以達到向標準輸入、輸出緩沖區中輸入的信息,能在網絡連接中發送的效果。
#include<unistd.h>
int dup( int file_descriptor );
int dup2( int file_descriptor_one, int file_descriptor_two );
- dup: 創建一個新的文件描述符,該文件描述符和原有文件描述符
file_descriptor
指向相同的文件、管道或網絡連接。返回的文件描述符總是取系統當前可用的最小整數值。 - dup2: 與
dup
類似,只是返回第一個不小于file_descriptor_two
的整數值。
兩個系統調用失敗時都返回 -1
,并設置 error
。
通過 dup
和 dup2
創建的文件描述符并不繼承原文件描述符的屬性。比如:close-on-exec
和 non-blocking
等。
用 dup
實現一個基本的 CGI服務器
的局部代碼:
close( STDOUT_FILENO );
dup( connfd );
printf( "hello\n" );
close( connfd );
流程:
- 先關閉標準輸出文件描述符
STDOUT_FILENO
(其值是1); - 通過
dup
復制socket
文件描述符connfd
; - 由于
dup
總是返回系統中最小的可用文件描述符,所以它的返回值實際上是1
,即之前關閉的標準輸出文件描述符的值; - 服務器輸出到標準輸出的內容(“hello”)就會直接發送到與客戶端對應的
socket
上,因此printf
調用的輸出將被客戶端獲得,而不是顯示在服務器程序的終端上。
讀取或寫入數據
readv函數、writev函數
- readv函數: 將數據從文件描述符讀到分散的內存塊中,即分散讀;
- writev函數: 將多塊分配的內存數據一并寫入文件描述符中,即集中寫。
相當于簡化版的 recvmsg
和 sendmsg
。
#include<sys/uio.h>
ssize_t readv( int fd, const struct iovec* vector, int count );
ssize_t weitev( int fd, const struct iovec* vector, int count );
// fd:被操作的目標文件描述符
// vector:iovec結構數組,iovec封裝了一塊內存的起始位置和長度
// count:vector數組的長度,即有多少塊內存數據需要從fd讀出或寫入到fd
// 成功時返回讀出/寫入fd的字節數,失敗則返回-1并試著errno。
零拷貝
sendfile函數
sendfile函數: 用于在兩個文件描述符之間直接傳遞數據(完全在內核中操作),從而避免了內核緩沖區和用戶緩沖區之間的數據拷貝,效率很高。被稱為 零拷貝。
#include<sys/sendfile.h>
ssize_t sendfile( int out_fd, int in_fd, off_t* offset, size_t count );
// out_fd:待寫入內容的文件描述符,必須是一個socket
// in_fd:待讀出內容的文件描述符,必須是一個支持類似mmap函數的文件描述符,即必須指向真實的文件,不能是socket和管道
// offset:指定從讀入文件流的哪個位置開始讀,如果為空,則使用讀入文件流默認的起始位置
// count:傳輸的字節數
// 成功時返回傳輸的字節數,失敗返回-1,并設置errno
sendfile
幾乎是專門為在網絡上傳輸文件而設計的。
splice函數
splice函數: 本質就是借助管道描述符在兩個文件之間移動數據,也是零拷貝。
#include<fcntl.h>
ssize_t splice( int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags );
// fd_in:待輸入數據的文件描述符
// off_in:如果fd_in是一個管道描述符,那么off_in必須被設置為NULL;反之,如果fd_in不是一個管道描述符(如socket),那么off_in表示從輸入數據流開始讀取數據的起始位置。總而言之,若off_in不為NULL,則它將指出具體的偏移位置。
// fd_out/off_out:與fd_in/off_in類似,不過用于輸出數據流。
// len:移動數據的長度
// flags:控制數據的移動方式
使用 splic函數
時 fd_in
和 fd_out
必須至少有一個是管道文件描述符。splice函數
調用成功時返回移動字節的數量。可能返回 0
,表示沒有數據需要移動,這發生在從管道中讀取數據(fd_in
是管道文件描述符)而管道沒有被寫入任何數據時(fd_out
不是管道文件描述符)。splice函數失敗時返回 -1
并設置 errno
。
常見的
errno
:
errno | 含義 |
---|---|
EBADF | 參數所指文件描述符有錯 |
ENOMEM | 內存不夠 |
EINVAL | 目標文件系統不支持splice,或者目標文件以追加方式打開,或者兩個文件描述符都不是管道文件描述符,或者某個 offset 參數被用于不支持隨機訪問的設備(如字符設備) |
ESPIPE | 參數 fd_in(或fd_out) 是管道文件描述符,而 off_in(或off_out) 不為NULL |
tee函數
tee函數: 兩個管道文件描述符之間的 零拷貝。它不消耗數據,因此源文件描述符上的數據仍可以用于后續的讀操作。
#include<fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
// 參數含義同splice,但 fd_in 和 fd_out 必須同時是管道文件描述符
// 成功時返回兩個文件之間復制的數據數量(字節數),返回0表示沒有復制任何數據,tee失敗返回-1并設置errno。
tee
常和 splice
一起用,splice
將非管道與管道綁定,tee
將 splice
操作后得到的管道綁定在一起。
進程間通信——共享內存
mmap函數 和 munmap函數
- mmap函數: 申請一段內存空間作為進程間通信的共享內存,也可以將文件直接映射到其中。
- munmap函數: 釋放由
mmap
創建的內存空間。
#include<sys/mman.h>
void* mmap( void* start, size_t length, int prot, int flags, int fd, off_t offset );
int munmap( void* start, size_t length );
// start:起始地址,為NULL時由系統自動分配一個地址。
// length:指定內存段的長度。
// prot:設置內粗段的訪問權限,可取以下值的按位或:
// PROT_READ,內存段可讀。
// PROT_WRITE,內存段可寫。
// PROT_EXEC,內存段可執行。
// PROT_NONE,內存段不能被訪問。
// flags:控制內存段內容被修改后程序的行為。
// fd:被映射文件對應的文件描述符,一般通過open調用獲得。
// offset:參數設置從文件的何處開始映射(對于不需要讀入整個文件的情況)。
// 成功時返回0,失敗返回-1并設置errno。
mmap 的 flags 參數的常用值及其含義:
常用值 | 含義 |
---|---|
MAP_SHARED | 進程間共享這段內存,對該內存段的修改將反映到被映射的文件中。提供了進程間共享內存的 POSIX 方法 |
MAP_PRIVATE | 內存段為調用進程所私有,所有修改不會反映到被映射的文件中 |
MAP_ANONYMOUS | 這段內存不是從文件映射而來的,其內容被初始化為全0。此時 mmap 最后兩個參數將被忽略。 |
MAP_FIXED | 內存段必須位于 start 指定的地址處,start 必須是內存頁面大小(4096字節=4KB)的整數倍 |
MAP_HUGETLB | 按照“大內存頁面”來分配內存空間,“大內存頁面”由 /proc/meminfo 文件來擦查看 |
控制文件描述符
fcntl函數
fcntl函數: 全名 file control
,提供了對文件描述符的各種控制操作(類似于系統調用 ioctl
,但 ioctl
比 fcntl
提供的控制更多。)但是 fcntl
是 POSIX
規定的首選方法。
#include<fcntl.h>
int fcntl(int fd, int cmd, ...);
// fd:被操作的文件描述符
// cmd:執行何種操作
// 有可能需要第三個可選參數 arg
// 成功時返回值根據操作不同有所不同,失敗時返回-1并設置errno
將文件描述符設置為非阻塞的:
int setnonblocking( int fd ){// F_GETFL 獲取 fd 的標志,成功時返回 fd 的標志int old_option = fcntl(fd, F_GETFL); // 獲取文件描述符舊的狀態標志int new_option = old_option | O_NONBLOCK; // 設置非阻塞標志fcntl(fd, F_SETFL, new_option); // F_SETFL 設置 fd 的標志return old_option; // 返回文件描述符舊的狀態標志,以便日后恢復該狀態標志
}
題外話:SIGIO
和 SIGURG
這兩個信號與其他 Linux
信號不同,他們必須與某個文件描述符相關聯方可使用:
- 被關聯文件描述符可讀或可寫時,系統將觸發
SIGIO
信號。 - 被關聯文件描述符是一個
socket
且有帶外數據可讀時,系統將觸發SIGURG
信號。
這兩個信號就是通過 fcntl函數
與文件描述符關聯的,具體做法是:fcntl函數
為目標文件描述符指定宿主進程或進程組,被指定的宿主進程或進程組去捕獲這兩個信號。
特別的,使用 SIGIO
時,還需利用 fcntl
設置其 O_ASYNC標志
(異步I/O標志,不過SIGIO信號模型并非真正意義上的異步I/O模型)。