Linux高性能服務(wù)器編程
參考
Linux高性能服務(wù)器編程源碼: https://github.com/raichen/LinuxServerCodes
豆瓣: Linux高性能服務(wù)器編程
第06章 高級(jí)I/O函數(shù)
Linux提供了很多高級(jí)的I/O函數(shù)。它們并不像Linux基礎(chǔ)I/O函數(shù)(比如open和read) 那么常用(編寫內(nèi)核模塊時(shí)一般要實(shí)現(xiàn)這些I/O函數(shù)),但在特定的條件下卻表現(xiàn)出優(yōu)秀的性 能。本章將討論其中和網(wǎng)絡(luò)編程相關(guān)的幾個(gè),這些函數(shù)大致分為三類:
用于創(chuàng)建文件描述符的函數(shù),包括pipe、dup/dup2函數(shù)。
用于讀寫數(shù)據(jù)的函數(shù),包括readv/writev、sendfile、mmap/munmap、splice和tee函數(shù)。
用于控制I/O行為和屬性的函數(shù),包括fcntl函數(shù)。
6.1 pipe函數(shù)
pipe函數(shù)可用于創(chuàng)建一個(gè)管道,以實(shí)現(xiàn)進(jìn)程間通信。我們將在13.4節(jié)討論如何使用管道 來(lái)實(shí)現(xiàn)進(jìn)程間通信,本章只介紹其基本使用方式。pipe函數(shù)的定義如下:
#include <unistd.h>
int pipe( int fd[2] );
pipe函數(shù)的參數(shù)是一個(gè)包含兩個(gè)int型整數(shù)的數(shù)組指針。該函數(shù)成功時(shí)返回0,并將一對(duì) 打開的文件描述符值填入其參數(shù)指向的數(shù)組。如果失敗,則返回-1并設(shè)置errno。
通過(guò)pipe函數(shù)創(chuàng)建的這兩個(gè)文件描述符fd[0]和fd[1]分別構(gòu)成管道的兩端,往fd[1]寫入的數(shù)據(jù)可以從fd[0]讀出。并且,fd[0]只能用于從管道讀出數(shù)據(jù),fd[1]則只能用于往管道 寫入數(shù)據(jù),而不能反過(guò)來(lái)使用。如果要實(shí)現(xiàn)雙向的數(shù)據(jù)傳輸,就應(yīng)該使用兩個(gè)管道。默認(rèn)情況下,這一對(duì)文件描述符都是阻塞的。此時(shí)如果我們用read系統(tǒng)調(diào)用來(lái)讀取一個(gè)空的管道, 則read將被阻塞,直到管道內(nèi)有數(shù)據(jù)可讀;如果我們用write系統(tǒng)調(diào)用來(lái)往一個(gè)滿的管道(見(jiàn) 后文)中寫入數(shù)據(jù),則write亦將被阻塞,直到管道有足夠多的空閑空間可用。但如果應(yīng)用 程序?qū)d[0]和fd[1]都設(shè)置為非阻塞的,則read和write會(huì)有不同的行為。關(guān)于阻塞和非阻 塞的討論,見(jiàn)第8章。如果管道的寫端文件描述符fd[1]的引用計(jì)數(shù)(見(jiàn)5.7節(jié))減少至0, 即沒(méi)有任何進(jìn)程需要往管道中寫入數(shù)據(jù),則針對(duì)該管道的讀端文件描述符fd[0]的read操作 將返回0,即讀取到了文件結(jié)束標(biāo)記(End Of File,EOF);反之,如果管道的讀端文件描述 符fd[0]的引用計(jì)數(shù)減少至0,即沒(méi)有任何進(jìn)程需要從管道讀取數(shù)據(jù),則針對(duì)該管道的寫端文 件描述符fd[1]的write操作將失敗,并引發(fā)SIGPIPE信號(hào)。關(guān)于SIGPIPE信號(hào),我們將在 第10章討論。
pipe
函數(shù)是用于創(chuàng)建管道的系統(tǒng)調(diào)用。管道是用于進(jìn)程間通信的一種機(jī)制,它可以在兩個(gè)進(jìn)程之間傳遞數(shù)據(jù)。pipe
函數(shù)的聲明如下:
#include <unistd.h>
int pipe(int fd[2]);
-
參數(shù):
-
fd
: 用于存儲(chǔ)管道兩端文件描述符的數(shù)組。fd[0]
是用于讀取的文件描述符,fd[1]
是用于寫入的文件描述符。
-
-
返回值:
- 如果成功,返回 0;如果失敗,返回 -1,并設(shè)置
errno
。
- 如果成功,返回 0;如果失敗,返回 -1,并設(shè)置
使用示例:
#include <stdio.h>
#include <unistd.h>
int main() {
int pipe_fd[2];
// 創(chuàng)建管道
if (pipe(pipe_fd) == -1) {
perror("pipe");
return 1;
}
// 管道創(chuàng)建成功,pipe_fd[0] 用于讀取,pipe_fd[1] 用于寫入
// 關(guān)閉不需要的文件描述符
close(pipe_fd[0]); // 關(guān)閉讀取端
close(pipe_fd[1]); // 關(guān)閉寫入端
return 0;
}
上述示例演示了如何使用 pipe
函數(shù)創(chuàng)建一個(gè)管道。創(chuàng)建成功后,pipe_fd[0]
用于讀取,pipe_fd[1]
用于寫入。通常,創(chuàng)建管道后,需要在進(jìn)程中關(guān)閉不需要的文件描述符。
管道內(nèi)部傳輸?shù)臄?shù)據(jù)是字節(jié)流,這和TCP字節(jié)流的概念相同。但二者又有細(xì)微的區(qū)別。應(yīng)用層程序能往一個(gè)TCP連接中寫入多少字節(jié)的數(shù)據(jù),取決于對(duì)方的接收通告窗口的大小和 本端的擁塞窗口的大小。而管道本身?yè)碛幸粋€(gè)容量限制,它規(guī)定如果應(yīng)用程序不將數(shù)據(jù)從管道讀走的話,該管道最多能被寫入多少字節(jié)的數(shù)據(jù)。自Linux2.6.11內(nèi)核起,管道容量的大 小默認(rèn)是65536字節(jié)。我們可以使用fcntl函數(shù)來(lái)修改管道容量(見(jiàn)后文)。此外,socket的基礎(chǔ)API中有一個(gè)socketpair 函數(shù)。它能夠方便地創(chuàng)建雙向管道。其定義如下:
#include<sys/types.h>
#include<sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2] );
socketpair 前三個(gè)參數(shù)的含義與socket系統(tǒng)調(diào)用的三個(gè)參數(shù)完全相同,但domain 只能使 用UNIX本地域協(xié)議族AF_UNIX,因?yàn)槲覀儍H能在本地使用這個(gè)雙向管道。最后一個(gè)參數(shù) 則和pipe系統(tǒng)調(diào)用的參數(shù)一樣,只不過(guò)socketpair創(chuàng)建的這對(duì)文件描述符都是既可讀又可寫 的。socketpair 成功時(shí)返回0,失敗時(shí)返回-1并設(shè)置errno。
6.2 dup函數(shù)和dup2函數(shù)
有時(shí)我們希望把標(biāo)準(zhǔn)輸入重定向到一個(gè)文件,或者把標(biāo)準(zhǔn)輸出重定向到一個(gè)網(wǎng)絡(luò)連接 (比如CGI編程)。這可以通過(guò)下面的用于復(fù)制文件描述符的dup或dup2函數(shù)來(lái)實(shí)現(xiàn):
#include <unistd.h>
int dup( int flle_descriptor );
int dup2( int file_descriptor_one, int file_descriptor_two );
dup函數(shù)創(chuàng)建一個(gè)新的文件描述符,該新文件描述符和原有文件描述符file_descriptor指 向相同的文件、管道或者網(wǎng)絡(luò)連接。并且dup返回的文件描述符總是取系統(tǒng)當(dāng)前可用的最小 整數(shù)值。dup2和dup類似,不過(guò)它將返回第一個(gè)不小于file_descriptor_two的整數(shù)值。dup和 dup2系統(tǒng)調(diào)用失敗時(shí)返回-1并設(shè)置errno。
6-1testdup.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(int argc, char* argv[]) {
// 檢查命令行參數(shù)是否足夠
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]);
// 初始化服務(wù)器地址結(jié)構(gòu)
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);
// 創(chuàng)建套接字
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
// 綁定地址
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
// 監(jiān)聽連接
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 {
// 關(guān)閉標(biāo)準(zhǔn)輸出文件描述符
close(STDOUT_FILENO);
// 復(fù)制 connfd 到標(biāo)準(zhǔn)輸出文件描述符的位置
dup(connfd);
// 此后標(biāo)準(zhǔn)輸出將輸出到 connfd 關(guān)聯(lián)的套接字
printf("abcd\n");
// 關(guān)閉 connfd
close(connfd);
}
// 關(guān)閉套接字
close(sock);
return 0;
}
這段代碼的主要作用是創(chuàng)建一個(gè)服務(wù)器程序,監(jiān)聽指定端口,并在接收到客戶端連接后將標(biāo)準(zhǔn)輸出重定向到與客戶端連接關(guān)聯(lián)的套接字。
-
socket 創(chuàng)建:通過(guò)
socket
函數(shù)創(chuàng)建一個(gè)套接字,用于接收客戶端連接。 -
bind 和 listen:使用
bind
綁定地址,然后通過(guò)listen
監(jiān)聽連接。 -
accept:等待客戶端連接,一旦有客戶端連接,就會(huì)返回一個(gè)新的套接字
connfd
。 -
dup 函數(shù):關(guān)閉標(biāo)準(zhǔn)輸出文件描述符 (
STDOUT_FILENO
),然后使用dup
函數(shù)將connfd
復(fù)制到標(biāo)準(zhǔn)輸出文件描述符的位置。這樣,之后所有的printf
輸出都將寫入到與客戶端連接關(guān)聯(lián)的套接字。 -
輸出到客戶端:通過(guò)
printf
輸出 “abcd”,這將通過(guò)與客戶端連接的套接字發(fā)送給客戶端。 -
關(guān)閉套接字:關(guān)閉套接字,釋放資源。
總體來(lái)說(shuō),這段代碼演示了如何將標(biāo)準(zhǔn)輸出重定向到與客戶端連接的套接字,從而實(shí)現(xiàn)通過(guò)網(wǎng)絡(luò)連接輸出信息到客戶端。
在代碼清單6-1中,我們先關(guān)閉標(biāo)準(zhǔn)輸出文件描述符STDOUT_FILENO(其值是1), 然后復(fù)制socket文件描述符connfd。因?yàn)閐up總是返回系統(tǒng)中最小的可用文件描述符,所以 它的返回值實(shí)際上是1,即之前關(guān)閉的標(biāo)準(zhǔn)輸出文件描述符的值。這樣一來(lái),服務(wù)器輸出到 標(biāo)準(zhǔn)輸出的內(nèi)容(這里是“abcd”)就會(huì)直接發(fā)送到與客戶連接對(duì)應(yīng)的socket上,因此printf 調(diào)用的輸出將被客戶端獲得(而不是顯示在服務(wù)器程序的終端上)。這就是CGl服務(wù)器的基 本工作原理。
這段話描述了CGI服務(wù)器的基本工作原理。下面是對(duì)每個(gè)步驟的解釋:
-
關(guān)閉標(biāo)準(zhǔn)輸出文件描述符 (
STDOUT_FILENO
): 通過(guò)調(diào)用close(STDOUT_FILENO)
關(guān)閉標(biāo)準(zhǔn)輸出文件描述符。這是因?yàn)樵贑GI服務(wù)器的工作模式中,我們希望將動(dòng)態(tài)生成的內(nèi)容發(fā)送到與客戶端連接相關(guān)聯(lián)的套接字,而不是輸出到服務(wù)器程序的終端。 -
復(fù)制socket文件描述符 (
connfd
): 使用dup(connfd)
將套接字文件描述符connfd
復(fù)制到系統(tǒng)中最小的可用文件描述符,而這個(gè)最小的可用文件描述符實(shí)際上就是關(guān)閉的標(biāo)準(zhǔn)輸出文件描述符STDOUT_FILENO
的值。這意味著現(xiàn)在套接字文件描述符connfd
成為了標(biāo)準(zhǔn)輸出文件描述符的副本。 -
輸出到標(biāo)準(zhǔn)輸出: 使用
printf
輸出內(nèi)容(在這里是 “abcd”)。由于標(biāo)準(zhǔn)輸出文件描述符已經(jīng)被復(fù)制為與客戶端連接相關(guān)的套接字,所以printf
的輸出實(shí)際上會(huì)被發(fā)送到客戶端而不是顯示在服務(wù)器程序的終端上。 -
客戶端接收: 因?yàn)闃?biāo)準(zhǔn)輸出已被重定向到與客戶端連接的套接字,所以客戶端將接收到服務(wù)器發(fā)送的 “abcd”。
總體而言,CGI服務(wù)器通過(guò)關(guān)閉標(biāo)準(zhǔn)輸出,將套接字文件描述符復(fù)制到標(biāo)準(zhǔn)輸出的位置,然后通過(guò)標(biāo)準(zhǔn)輸出輸出內(nèi)容,實(shí)現(xiàn)了將動(dòng)態(tài)生成的內(nèi)容發(fā)送到與客戶端連接相關(guān)的套接字,從而向客戶端提供實(shí)時(shí)的動(dòng)態(tài)內(nèi)容。這是基本的CGI服務(wù)器工作原理。
6.3 readv 函數(shù)和writev 函數(shù)
readv函數(shù)將數(shù)據(jù)從文件描述符讀到分散的內(nèi)存塊中,即分散讀;writev函數(shù)則將多塊分散的內(nèi)存數(shù)據(jù)一并寫入文件描述符中,即集中寫。它們的定義如下:
#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 count );
fd參數(shù)是被操作的目標(biāo)文件描述符。vector參數(shù)的類型是iovec結(jié)構(gòu)數(shù)組。我們?cè)诘? 章討論過(guò)結(jié)構(gòu)體iovec,該結(jié)構(gòu)體描述一塊內(nèi)存區(qū)。count參數(shù)是vector數(shù)組的長(zhǎng)度,即有多 少塊內(nèi)存數(shù)據(jù)需要從fd讀出或?qū)懙絝d。readv和writev在成功時(shí)返回讀出/寫入fd的字節(jié)數(shù),失敗則返回-1并設(shè)置errno。它們相當(dāng)于簡(jiǎn)化版的recvmsg和sendmsg函數(shù)。
考慮第4章討論過(guò)的Web服務(wù)器。當(dāng)Web服務(wù)器解析完一個(gè)HTTP請(qǐng)求之后,如果目標(biāo)文檔存在且客戶具有讀取該文檔的權(quán)限,那么它就需要發(fā)送一個(gè)HTTP應(yīng)答來(lái)傳輸該文檔。這個(gè)HTTP應(yīng)答包含1個(gè)狀態(tài)行、多個(gè)頭部字段、1個(gè)空行和文檔的內(nèi)容。其中,前3部分的內(nèi)容可能被Web服務(wù)器放置在一塊內(nèi)存中,而文檔的內(nèi)容則通常被讀入到另外一塊單 獨(dú)的內(nèi)存中(通過(guò)read函數(shù)或mmap函數(shù))。我們并不需要把這兩部分內(nèi)容拼接到一起再發(fā)送,而是可以使用writev函數(shù)將它們同時(shí)寫出,如代碼清單6-2所示。
6-2testwritev.cpp
這段代碼是一個(gè)簡(jiǎn)單的HTTP服務(wù)器,根據(jù)客戶端請(qǐng)求的文件名,在響應(yīng)中返回相應(yīng)的文件內(nèi)容。以下是對(duì)代碼的注釋和解釋:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#define BUFFER_SIZE 1024
static const char* status_line[2] = { "200 OK", "500 Internal server error" };
int main( int argc, char* argv[] )
{
// 檢查命令行參數(shù)
if( argc <= 3 )
{
printf( "usage: %s ip_address port_number filename\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
const char* file_name = argv[3];
// 創(chuàng)建套接字
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( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
// 綁定地址
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
// 監(jiān)聽
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
{
// 處理HTTP響應(yīng)
char header_buf[ BUFFER_SIZE ];
memset( header_buf, '\0', BUFFER_SIZE );
char* file_buf;
struct stat file_stat;
bool valid = true;
int len = 0;
// 檢查文件狀態(tài)
if( stat( file_name, &file_stat ) < 0 )
{
valid = false;
}
else
{
// 檢查是否為目錄,是否有讀權(quán)限
if( S_ISDIR( file_stat.st_mode ) || !(file_stat.st_mode & S_IROTH) )
{
valid = false;
}
else
{
// 讀取文件內(nèi)容
int fd = open( file_name, O_RDONLY );
file_buf = new char [ file_stat.st_size + 1 ];
memset( file_buf, '\0', file_stat.st_size + 1 );
if ( read( fd, file_buf, file_stat.st_size ) < 0 )
{
valid = false;
}
}
}
if( valid )
{
// 構(gòu)建HTTP響應(yīng)頭
ret = snprintf( header_buf, BUFFER_SIZE-1, "%s %s\r\n", "HTTP/1.1", status_line[0] );
len += ret;
ret = snprintf( header_buf + len, BUFFER_SIZE-1-len,
"Content-Length: %d\r\n", file_stat.st_size );
len += ret;
ret = snprintf( header_buf + len, BUFFER_SIZE-1-len, "%s", "\r\n" );
struct iovec iv[2];
iv[ 0 ].iov_base = header_buf;
iv[ 0 ].iov_len = strlen( header_buf );
iv[ 1 ].iov_base = file_buf;
iv[ 1 ].iov_len = file_stat.st_size;
// 使用 writev 函數(shù)將響應(yīng)頭和文件內(nèi)容一并寫入套接字
ret = writev( connfd, iv, 2 );
}
else
{
// 發(fā)送500錯(cuò)誤響應(yīng)
ret = snprintf( header_buf, BUFFER_SIZE-1, "%s %s\r\n", "HTTP/1.1", status_line[1] );
len += ret;
ret = snprintf( header_buf + len, BUFFER_SIZE-1-len, "%s", "\r\n" );
send( connfd, header_buf, strlen( header_buf ), 0 );
}
// 關(guān)閉連接并釋放資源
close( connfd );
delete [] file_buf;
}
// 關(guān)閉服務(wù)器套接字
close( sock );
return 0;
}
這個(gè)程序根據(jù)客戶端請(qǐng)求的文件名,返回相應(yīng)的HTTP響應(yīng)。它能處理的請(qǐng)求包括:
- 如果請(qǐng)求的文件存在且可讀,返回一個(gè)包含文件內(nèi)容的200 OK響應(yīng)。
- 如果請(qǐng)求的文件是目錄或者不可讀,返回一個(gè)500 Internal Server Error響應(yīng)。
代碼清單6-2中,我們省略了HTTP請(qǐng)求的接收及解析,因?yàn)楝F(xiàn)在關(guān)注的重點(diǎn)是HTTP 應(yīng)答的發(fā)送。我們直接將目標(biāo)文件作為第3個(gè)參數(shù)傳遞給服務(wù)器程序,客戶telnet到該服務(wù) 器上即可獲得該文件。關(guān)于HTTP請(qǐng)求的解析,我們將在第8章給出相關(guān)代碼。
6.4 sendfile 函數(shù)
sendfile函數(shù)在兩個(gè)文件描述符之間直接傳遞數(shù)據(jù)(完全在內(nèi)核中操作),從而避免了內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的數(shù)據(jù)拷貝,效率很高,這被稱為零拷貝。sendfile函數(shù)的定義如下:
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count );
in_fd參數(shù)是待讀出內(nèi)容的文件描述符,out_fd參數(shù)是待寫入內(nèi)容的文件描述符。offset參數(shù)指定從讀入文件流的哪個(gè)位置開始讀,如果為空,則使用讀入文件流默認(rèn)的起始位置。 count參數(shù)指定在文件描述符in_fd和out_fd之間傳輸?shù)淖止?jié)數(shù)。sendfile 成功時(shí)返回傳輸?shù)?字節(jié)數(shù),失敗則返回-1并設(shè)置errno。該函數(shù)的man手冊(cè)明確指出,in_fd必須是一個(gè)支持 類似mmap函數(shù)的文件描述符,即它必須指向真實(shí)的文件,不能是socket和管道;而out fd 則必須是一個(gè)socket。由此可見(jiàn),sendfile幾乎是專門為在網(wǎng)絡(luò)上傳輸文件而設(shè)計(jì)的。下面的 代碼清單6-3利用sendfile函數(shù)將服務(wù)器上的一個(gè)文件傳送給客戶端。
以下是一個(gè)簡(jiǎn)單的使用 sendfile
函數(shù)的代碼示例。該示例將一個(gè)文件的內(nèi)容寫入到套接字中。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <unistd.h>
int main() {
// 打開源文件
int in_fd = open("source.txt", O_RDONLY);
if (in_fd == -1) {
perror("Error opening source file");
return 1;
}
// 創(chuàng)建套接字并綁定端口
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 省略套接字創(chuàng)建和綁定的代碼
// 打開目標(biāo)文件(套接字)
int out_fd = accept(sock, NULL, NULL);
if (out_fd == -1) {
perror("Error accepting connection");
close(in_fd);
close(sock);
return 1;
}
// 獲取源文件的大小
struct stat stat_buf;
fstat(in_fd, &stat_buf);
// 使用 sendfile 將文件內(nèi)容傳輸?shù)教捉幼?/span>
off_t offset = 0;
ssize_t sent_bytes = sendfile(out_fd, in_fd, &offset, stat_buf.st_size);
if (sent_bytes == -1) {
perror("Error using sendfile");
}
// 關(guān)閉文件和套接字
close(in_fd);
close(out_fd);
close(sock);
return 0;
}
請(qǐng)注意,上述代碼是一個(gè)簡(jiǎn)化的示例,實(shí)際應(yīng)用中可能需要更多的錯(cuò)誤檢查和處理。
上述代碼中的
struct stat stat_buf;
fstat(in_fd, &stat_buf);
解釋如下:
struct stat stat_buf;
聲明了一個(gè)結(jié)構(gòu)體變量 stat_buf
,該結(jié)構(gòu)體用于存儲(chǔ)文件的狀態(tài)信息,包括文件大小、權(quán)限、最后訪問(wèn)時(shí)間等。fstat(in_fd, &stat_buf);
通過(guò)文件描述符 in_fd
獲取文件狀態(tài)信息,并將其保存在 stat_buf
中。
具體而言,fstat
函數(shù)的作用是獲取與文件描述符相關(guān)聯(lián)的文件的狀態(tài)信息,并將這些信息填充到傳入的結(jié)構(gòu)體中。在這里,fstat
函數(shù)用于獲取打開的源文件 in_fd
的狀態(tài)信息,以便后續(xù)操作,如獲取文件大小等。
struct stat
結(jié)構(gòu)體的定義通常包含了很多字段,例如:
struct stat {
dev_t st_dev; /* 文件所在設(shè)備的 ID */
ino_t st_ino; /* 文件的 inode 號(hào) */
mode_t st_mode; /* 文件的類型和權(quán)限信息 */
nlink_t st_nlink; /* 文件的硬鏈接數(shù)量 */
uid_t st_uid; /* 文件的用戶 ID */
gid_t st_gid; /* 文件的組 ID */
off_t st_size; /* 文件的大?。ㄗ止?jié)數(shù))*/
time_t st_atime; /* 最后訪問(wèn)時(shí)間 */
time_t st_mtime; /* 最后修改時(shí)間 */
time_t st_ctime; /* 最后狀態(tài)改變時(shí)間 */
blksize_t st_blksize; /* 文件系統(tǒng) I/O 緩沖區(qū)大小 */
blkcnt_t st_blocks; /* 分配給文件的塊數(shù)量 */
};
在上述代碼中,st_size
字段用于獲取文件的大?。ㄗ止?jié)數(shù)),這對(duì)于確定文件的長(zhǎng)度非常有用。
6-3testsendfile.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
int main( int argc, char* argv[] )
{
// 檢查命令行參數(shù)
if( argc <= 3 )
{
printf( "usage: %s ip_address port_number filename\n", basename( argv[0] ) );
return 1;
}
// 獲取命令行參數(shù)
const char* ip = argv[1];
int port = atoi( argv[2] );
const char* file_name = argv[3];
// 打開文件
int filefd = open( file_name, O_RDONLY );
assert( filefd > 0 );
// 獲取文件狀態(tài)信息
struct stat stat_buf;
fstat( filefd, &stat_buf );
// 創(chuàng)建服務(wù)器地址結(jié)構(gòu)
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 );
// 創(chuàng)建監(jiān)聽socket
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
// 綁定地址
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
// 監(jiān)聽
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
{
// 使用sendfile發(fā)送文件內(nèi)容
sendfile( connfd, filefd, NULL, stat_buf.st_size );
// 關(guān)閉連接
close( connfd );
}
// 關(guān)閉監(jiān)聽socket
close( sock );
return 0;
}
代碼清單6-3中,我們將目標(biāo)文件作為第3個(gè)參數(shù)傳遞給服務(wù)器程序,客戶telnet到該服 務(wù)器上即可獲得該文件。相比代碼清單6-2,代碼清單6-3沒(méi)有為目標(biāo)文件分配任何用戶空間 的緩存,也沒(méi)有執(zhí)行讀取文件的操作,但同樣實(shí)現(xiàn)了文件的發(fā)送,其效率顯然要高得多。
6.5 mmap 函數(shù)和munmap函數(shù)
mmap函數(shù)用于申請(qǐng)一段內(nèi)存空間。我們可以將這段內(nèi)存作為進(jìn)程間通信的共享內(nèi)存, 也可以將文件直接映射到其中。munmap函數(shù)則釋放由mmap創(chuàng)建的這段內(nèi)存空間。它們的 定義如下:
#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參數(shù)允許用戶使用某個(gè)特定的地址作為這段內(nèi)存的起始地址。如果它被設(shè)置成 NULL,則系統(tǒng)自動(dòng)分配一個(gè)地址。length參數(shù)指定內(nèi)存段的長(zhǎng)度。prot參數(shù)用來(lái)設(shè)置內(nèi)存段的訪問(wèn)權(quán)限。它可以取以下幾個(gè)值的按位或:
PROT_READ,內(nèi)存段可讀。
PROT_WRITE,內(nèi)存段可寫。
PROT_EXEC,內(nèi)存段可執(zhí)行。
PROT NONE,內(nèi)存段不能被訪問(wèn)。
flags參數(shù)控制內(nèi)存段內(nèi)容被修改后程序的行為。它可以被設(shè)置為表6-1中的某些值(這 里僅列出了常用的值)的按位或(其中MAP_SHARED和MAP_PRIVATE是互斥的,不能同時(shí)指定)。
fd參數(shù)是被映射文件對(duì)應(yīng)的文件描述符。它一般通過(guò)open系統(tǒng)調(diào)用獲得。offset參數(shù)設(shè) 置從文件的何處開始映射(對(duì)于不需要讀入整個(gè)文件的情況)。mmap函數(shù)成功時(shí)返回指向目標(biāo)內(nèi)存區(qū)域的指針,失敗則返回MAP_FAILED((void*)-1)并設(shè)置errno。munmap函數(shù)成功時(shí)返回0,失敗則返回-1并設(shè)置errno。我們將在第13章進(jìn)一步討論如何利用mmap 函數(shù)實(shí)現(xiàn)進(jìn)程間共享內(nèi)存。
mmap
函數(shù)用于將一個(gè)文件或者其它對(duì)象映射到調(diào)用進(jìn)程的地址空間,而 munmap
函數(shù)用于解除這種映射關(guān)系。
-
mmap
函數(shù)參數(shù)解釋:-
start
: 指定映射的起始地址,通常設(shè)置為0,由系統(tǒng)自動(dòng)分配。 -
length
: 映射的長(zhǎng)度。 -
prot
: 保護(hù)標(biāo)志,指定映射區(qū)的保護(hù)方式,可以是PROT_NONE
(不可訪問(wèn)),PROT_READ
(可讀),PROT_WRITE
(可寫),PROT_EXEC
(可執(zhí)行)等。 -
flags
: 映射區(qū)的類型和映射對(duì)象的處理方式,可以是MAP_SHARED
(共享映射)或MAP_PRIVATE
(私有映射)等。 -
fd
: 文件描述符,映射的文件。 -
offset
: 文件映射的起始位置。
-
-
munmap
函數(shù)參數(shù)解釋:-
start
: 映射區(qū)的起始地址。 -
length
: 映射區(qū)的長(zhǎng)度。
-
以下是一個(gè)簡(jiǎn)單的代碼示例:
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
const char* file_path = "example.txt";
const size_t file_size = 4096;
// 打開文件
int fd = open(file_path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
return 1;
}
// 調(diào)整文件大小
if (ftruncate(fd, file_size) == -1) {
perror("ftruncate");
close(fd);
return 1;
}
// 映射文件到內(nèi)存
void* mapped_data = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped_data == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 將數(shù)據(jù)寫入映射區(qū)
const char* message = "Hello, Memory-mapped File!";
strncpy(mapped_data, message, strlen(message));
// 解除映射關(guān)系
if (munmap(mapped_data, file_size) == -1) {
perror("munmap");
}
// 關(guān)閉文件
close(fd);
return 0;
}
此示例創(chuàng)建了一個(gè)文件,通過(guò) mmap
將文件映射到內(nèi)存中,然后寫入數(shù)據(jù),最后通過(guò) munmap
解除映射關(guān)系。
6.6 splice 函數(shù)
splice函數(shù)用于在兩個(gè)文件描述符之間移動(dòng)數(shù)據(jù),也是零拷貝操作。splice函數(shù)的定義如下:
#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參數(shù)是待輸入數(shù)據(jù)的文件描述符。如果fd_in是一個(gè)管道文件描述符,那么off_in 參數(shù)必須被設(shè)置為NULL。如果fd_in不是一個(gè)管道文件描述符(比如socket),那么off_in表示從輸入數(shù)據(jù)流的何處開始讀取數(shù)據(jù)。此時(shí),若off_in被設(shè)置為NULL,則表示從輸入數(shù)據(jù)流的當(dāng)前偏移位置讀入;若off_in不為NULL,則它將指出具體的偏移位置。fd_out/off_ out參數(shù)的含義與fd_in/off_in相同,不過(guò)用于輸出數(shù)據(jù)流。len參數(shù)指定移動(dòng)數(shù)據(jù)的長(zhǎng)度; flags參數(shù)則控制數(shù)據(jù)如何移動(dòng),它可以被設(shè)置為表6-2中的某些值的按位或。
使用 splice 函數(shù)時(shí), fd_in和fd_out 必須至少有一個(gè)是管道文件描述符。splice 函數(shù)調(diào) 用成功時(shí)返回移動(dòng)字節(jié)的數(shù)量。它可能返回0,表示沒(méi)有數(shù)據(jù)需要移動(dòng),這發(fā)生在從管道中 讀取數(shù)據(jù)(fd_in是管道文件描述符)而該管道沒(méi)有被寫入任何數(shù)據(jù)時(shí)。splice函數(shù)失敗時(shí)返 回-1并設(shè)置errmo。常見(jiàn)的errno如表6-3所示。
下面我們使用splice函數(shù)來(lái)實(shí)現(xiàn)一個(gè)零拷貝的回射服務(wù)器,它將客戶端發(fā)送的數(shù)據(jù)原樣 返回給客戶端,具體實(shí)現(xiàn)如代碼清單6-4所示。
6-4testsplice.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
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] );
// 創(chuàng)建套接字
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( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
// 綁定套接字
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
// 監(jiān)聽套接字
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
{
// 創(chuàng)建管道
int pipefd[2];
ret = pipe( pipefd );
assert( ret != -1 );
// 從套接字讀取數(shù)據(jù)并通過(guò) splice 復(fù)制到管道
ret = splice( connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
// 從管道讀取數(shù)據(jù)并通過(guò) splice 復(fù)制到套接字
ret = splice( pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
// 關(guān)閉連接套接字
close( connfd );
}
// 關(guān)閉監(jiān)聽套接字
close( sock );
return 0;
}
注釋和解釋:
-
創(chuàng)建套接字、綁定、監(jiān)聽:通過(guò)
socket
、bind
、listen
創(chuàng)建并設(shè)置服務(wù)器套接字。 -
接受客戶端連接:使用
accept
函數(shù)等待客戶端連接,得到連接套接字connfd
。 -
創(chuàng)建管道:使用
pipe
創(chuàng)建一個(gè)管道,pipefd[0]
是讀取端,pipefd[1]
是寫入端。 -
通過(guò)
splice
實(shí)現(xiàn)數(shù)據(jù)傳輸:使用兩次splice
函數(shù),第一次從連接套接字connfd
中讀取數(shù)據(jù)并寫入管道,第二次從管道讀取數(shù)據(jù)并寫入連接套接字。這樣實(shí)現(xiàn)了零拷貝,避免了數(shù)據(jù)在用戶空間和內(nèi)核空間之間的復(fù)制。 -
關(guān)閉連接套接字:關(guān)閉已經(jīng)處理完的連接套接字。
-
關(guān)閉監(jiān)聽套接字:關(guān)閉服務(wù)器監(jiān)聽套接字。
我們通過(guò)splice函數(shù)將客戶端的內(nèi)容讀入到pipefd[1]中,然后再使用splice 函數(shù)從 pipefd[0]中讀出該內(nèi)容到客戶端,從而實(shí)現(xiàn)了簡(jiǎn)單高效的回射服務(wù)。整個(gè)過(guò)程未執(zhí)行recv/ send操作,因此也未涉及用戶空間和內(nèi)核空間之間的數(shù)據(jù)拷貝。
6.7 tee函數(shù)
tee函數(shù)在兩個(gè)管道文件描述符之間復(fù)制數(shù)據(jù),也是零拷貝操作。它不消耗數(shù)據(jù),因此 源文件描述符上的數(shù)據(jù)仍然可以用于后續(xù)的讀操作。tee函數(shù)的原型如下:
#include <fcntl.h>
ssize_t tee( int fd_in, int fd_out, size_t len, unsigned int flags );
該函數(shù)的參數(shù)的含義與splice相同(但fd_in和fd_out 必須都是管道文件描述符)。tee 函數(shù)成功時(shí)返回在兩個(gè)文件描述符之間復(fù)制的數(shù)據(jù)數(shù)量(字節(jié)數(shù))。返回0表示沒(méi)有復(fù)制任 何數(shù)據(jù)。tee失敗時(shí)返回-1并設(shè)置errno。
代碼清單6-5利用tee 函數(shù)和splice函數(shù),實(shí)現(xiàn)了Linux下tee程序(同時(shí)輸出數(shù)據(jù)到終 端和文件的程序,不要和tee函數(shù)混淆)的基本功能。
6-5testtee.cpp
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main( int argc, char* argv[] )
{
// 檢查命令行參數(shù)是否合法
if ( argc != 2 )
{
printf( "usage: %s <file>\n", argv[0] );
return 1;
}
// 打開文件,若文件不存在則創(chuàng)建
int filefd = open( argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666 );
assert( filefd > 0 );
// 創(chuàng)建兩個(gè)管道,分別用于標(biāo)準(zhǔn)輸入、文件寫入和標(biāo)準(zhǔn)輸出
int pipefd_stdout[2];
int ret = pipe( pipefd_stdout );
assert( ret != -1 );
int pipefd_file[2];
ret = pipe( pipefd_file );
assert( ret != -1 );
// 使用 splice 將標(biāo)準(zhǔn)輸入的內(nèi)容寫入 pipefd_stdout[1] 管道
ret = splice( STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
// 使用 tee 函數(shù)將 pipefd_stdout[0] 管道的內(nèi)容同時(shí)寫入 pipefd_file[1] 管道和標(biāo)準(zhǔn)輸出
ret = tee( pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK );
assert( ret != -1 );
// 使用 splice 將 pipefd_file[0] 管道的內(nèi)容寫入文件
ret = splice( pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
// 使用 splice 將 pipefd_stdout[0] 管道的內(nèi)容寫入標(biāo)準(zhǔn)輸出
ret = splice( pipefd_stdout[0], NULL, STDOUT_FILENO, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
// 關(guān)閉文件和所有使用的管道
close( filefd );
close( pipefd_stdout[0] );
close( pipefd_stdout[1] );
close( pipefd_file[0] );
close( pipefd_file[1] );
return 0;
}
作用: 該程序通過(guò) splice
和 tee
函數(shù)實(shí)現(xiàn)了將標(biāo)準(zhǔn)輸入的內(nèi)容同時(shí)寫入文件和標(biāo)準(zhǔn)輸出的功能。使用管道和文件描述符傳輸數(shù)據(jù),無(wú)需用戶空間和內(nèi)核空間之間的數(shù)據(jù)拷貝,從而提高了效率。
6.8 fcntl函數(shù)
fcntl 函數(shù),正如其名字(file control)描述的那樣,提供了對(duì)文件描述符的各種控制操 作。另外一個(gè)常見(jiàn)的控制文件描述符屬性和行為的系統(tǒng)調(diào)用是ioctl,而且ioctl比f(wàn)cntl能夠 執(zhí)行更多的控制。但是,對(duì)于控制文件描述符常用的屬性和行為,fcntl函數(shù)是由POSIX規(guī) 范指定的首選方法。所以本書僅討論fcntl 函數(shù)。fcntl函數(shù)的定義如下:
#include <fcntl.h>
int fcntl( int fd, int cmd, … );
fd參數(shù)是被操作的文件描述符,cmd參數(shù)指定執(zhí)行何種類型的操作。根據(jù)操作類型的不 同,該函數(shù)可能還需要第三個(gè)可選參數(shù)arg。
#include <fcntl.h>
int fcntl( int fd, int cmd, ... );
解釋:
-
fd
: 文件描述符,是需要進(jìn)行操作的文件或套接字的標(biāo)識(shí)符。 -
cmd
: 控制命令,指定對(duì)文件描述符fd
進(jìn)行的操作。
fcntl
函數(shù)用于對(duì)文件描述符進(jìn)行各種控制操作,取決于 cmd
參數(shù)的值。該函數(shù)的第三個(gè)參數(shù) arg
的具體含義取決于 cmd
的值。
常見(jiàn)的 cmd
可選值:
-
F_DUPFD
: 復(fù)制文件描述符。arg
為新的文件描述符的最小允許值。 -
F_GETFL
: 獲取文件狀態(tài)標(biāo)志。arg
為無(wú)符號(hào)整數(shù),表示文件的狀態(tài)標(biāo)志。 -
F_SETFL
: 設(shè)置文件狀態(tài)標(biāo)志。arg
為要設(shè)置的狀態(tài)標(biāo)志的位掩碼。 -
F_GETLK
: 獲取文件鎖信息。arg
為指向struct flock
結(jié)構(gòu)的指針,用于存儲(chǔ)鎖信息。 -
F_SETLK
: 設(shè)置文件鎖。arg
為指向struct flock
結(jié)構(gòu)的指針,用于設(shè)置鎖信息。 -
F_SETLKW
: 設(shè)置文件鎖,如果無(wú)法獲取鎖則阻塞。arg
為指向struct flock
結(jié)構(gòu)的指針。
示例:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
// 獲取文件狀態(tài)標(biāo)志
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl");
close(fd);
return 1;
}
// 設(shè)置文件狀態(tài)標(biāo)志,添加 O_APPEND 標(biāo)志
flags |= O_APPEND;
int result = fcntl(fd, F_SETFL, flags);
if (result == -1) {
perror("fcntl");
close(fd);
return 1;
}
// 其他操作...
close(fd);
return 0;
}
上述示例中,通過(guò) fcntl
函數(shù)獲取文件的狀態(tài)標(biāo)志,然后設(shè)置了 O_APPEND
標(biāo)志,將文件設(shè)置為以追加方式打開。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-812123.html
后記
截至2024年1月20日11點(diǎn)21分,學(xué)習(xí)完《Linux高性能服務(wù)器編程》第六章的內(nèi)容,主要介紹Linux的基礎(chǔ)I/O函數(shù)。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-812123.html
到了這里,關(guān)于《Linux高性能服務(wù)器編程》筆記02的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!