第六章:linux高級i/o函數
網絡I/O一直是Linux網絡編程中極其重要的一部分,除了前面講到的send、recv等,socket編程接口還給出了很多高級了I/O函數,這些函數大致分爲三類:
-
用於創建文件描述符的函數,包括pipe,dup/dup2等
-
用於讀寫數據的函數,包括readv/writev,sendfile,mmap/munmap,splice和tee
-
用於控制i/o行爲和屬性的函數,包括fcntl函數
一.pipe函數
pipe函數用於創建一個管道,以實現進程間的通信。
#include <unistd.h>
int pipe(int fd[2])
pipe函數將打開的文件描述符填入他的參數指向的數組。
通過pipe函數可以將創建的這兩個文件描述符分別構成管道的兩端,fd[0]只能用於從管道讀出數據,fd[1]只能用於從管道讀入數據。
fd[0]和fd[1]這兩個的本質都是文件描述符,當進程間有數據要傳輸時,數據發送的一端需要關閉fd[0],接收端要關閉fd[1],才能正常傳送數據。需要注意的是無名管道只能用低級文件編程庫中的讀寫函數進行操作,如read和write,當我們向一個空管道執行read時,函數會阻塞,直到有數據寫入才繼續執行,同理對滿的管道執行write也會進入阻塞狀態。但是如果對於這兩個文件描述符設置爲非阻塞模式,則他們會有不同的行爲。如果fd[1]的引用計數減少至0,即沒有寫端進程向管道中寫,則fd[0]上的read操作將會讀取到EOF標誌,返回0;反之如果fd[0]上的引用計數減少至0,即沒有讀端程序調用read,則此時fd[1]上的write操作將失敗並引發SIGPIPE信號。
爲了便於使用,API中還有一個函數用來創建雙向管道,是socketpair函數,使用這個函數創建的雙向管道只能使用AF_UNIX協議,即UNIX本地域協議族,它創建的兩個文件描述符是既可讀又可寫的。
二.dup/dup2函數
dup/dup2函數完成一個重要的操作:完成輸入輸出的重定向。有時我們希望將標準輸入輸出重定向到文件或者網絡連接,就可以通過以下函數實現
#include <unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one,int file_descriptor_two);
dup函數創建一個新的文件描述符,新的描述符與原文件描述符file_descriptor指向相同的文件,管道或者文件連接。我們來看一個CGI服務器的例子:(CGI服務器:輸入輸出重定向服務器)
#include"head.h"
using namespace std;
int main(int argc, char **argv) {
if(argc <= 2) {
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, 5);
assert(ret != -1);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd < 0) {
printf("errno is: %d\n", errno);
}
else {
close(STDOUT_FILENO);
int newfd = dup(connfd);
printf("abcd\n");
close(connfd);
}
close(sock);
return 0;
}
使用telnet客戶端連接服務器發現有abcd的回顯,通過這個例子我們可以看到,我們關閉了標準輸出文件描述符後再調用dup,會將要複製的connfd複製到當前未使用的最小的文件描述符也就是標準輸出文件描述符上,實現了輸出的重定向。(也就是服務器的printf重定向到了客戶端)
三.readv函數與writev函數
#include <sys/uio.h>
ssize_t readv(int fd,const struct iovec* vector,int count);
ssize_t writev(int fd,const struct iovec* vector,int court);
readv是將數據從文件描述符讀到分散的內存塊中,即分散讀;
writev是將數據從分散的內存塊一併讀入到文件描述符中,即集中寫。
一個典型的應用場景:在Web服務器解析完HTTP請求後如果客戶端請求的文件存在並且有權限時,就需要返回一個HTTP首部狀態碼和狀態信息,然後再返回該文件,但是我們考慮效率問題,如果每次我們都需要將兩個不相關的存儲空間合併到一起再發送勢必會很影響效率,所以我們可以事先將HTTP不同的頭部存儲好,找到文件後使用sendv函數直接發送即可。
四.sendfile函數
sendfile函數可以進行兩個文件描述符之間直接傳遞數據(完全在內核中實現),效率很高,被稱爲“零拷貝”。
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd,int in_fd,off_t* offset,size_t count);
in_fd是讀出內容的文件描述符,而out_fd是寫入的描述符。in_fd必須是一個真實的文件描述符,不能是socket或者pipe;而out_fd必須是一個socket。所以sendfile幾乎是專門爲在網絡上傳輸文件而設計的。
五.mmap函數和munmap函數
mmap函數用於申請一段內存空間,我們可以將這段內存作爲進程間通信的共享內存(進程間通信的另一種方法),也可以將文件直接映射到其中。munmap是mmap的反操作,釋放mmap申請的內存空間。
#include <sys/mmap.h>
void *mmap(void *start,size_t length,int port,int flags,int fd,off_t offset);
int munmap(void *start,size_t length);
port用於設置內存段的訪問權限,而flags設置內存被修改後程序的行爲。之後會細講mmap的進程間共享內存的實現。
六.splite函數
splite函數在兩個文件描述符之間移動數據,也是零拷貝的操作。
#include <fcntl.h>
ssize_t splite(int fd_in,loff_t* off_in,int fd_out,loff_t* off_out,size_t len,unsigned int flags);
但是其in_fd和out_fd中必須至少有一個管道文件描述符,調用成功時返回一共轉移的字節數。
同類型的函數還有tee,定義如下:
#include <fcntl.h>
ssize_t tee(int fd_in,int fd_out,size_t len,unsigned int flags)
tee的作用是在兩個管道文件符之間複製數據,也是零拷貝操作,他不消耗數據,因此源文件描述符上的數據仍然可以用作接下來的操作。
七.fcntl函數
fcntl函數,全名爲file control函數,就像名字那樣提供了對文件描述符的各種控制作用。另外一個常用的控制文件描述符屬性函數爲ioctl,而且ioctl比fcntl能夠進行更多的操作。具體fcntl的參數很多,如果需要可以進行相關資料的查詢。