重新導向dup和dup2函數
#include <unistd.h>int dup(int file_descriptor);int dup2(int file_descriptor_one, int file_descriptor_two);
- dup建立一個新的檔案描述符, 此描述符和原有的file_descriptor指向相同的檔案、管道或者網路連接。
- dup返回的檔案描述符總是取系統當前可用的最小整數值。
dup2函數通過使用參數file_descriptor_two指定新描述符數值,如果file_descriptor_two已經開啟,則先關閉。若file_descriptor_one = file_descriptor_two,而不關閉。
兩者調用失敗均返回-1, 並設定errno。
//利用dup類比實現一個基本的CGI伺服器#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <errno.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>#include <assert.h>int main(int argc, char **argv){if (argc != 3) { fprintf(stderr, "Usage: %s id port\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;address.sin_port = htons(port);inet_pton(AF_INET, ip, &address.sin_addr);int sock = socket(PF_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("error: %s\n", strerror(errno));}else {close(STDOUT_FILENO); //關閉標準輸出dup(connfd); //重新導向1到connfd,這樣伺服器的標準輸出內容會直接發送到用戶端socket,這就是CGI的基本原理printf("abc. close stdout_fileno test... dup to client\n"); //printf會直接輸出會發送到用戶端close(connfd);}close(sock);return 0;}
分散讀readv和集中寫writev函數
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 count);
fd參數是被操作的檔案描述符。
vector參數是iovec結構體:
#include <sys/uio.h>struct iovec{ void *iov_base; //指向一個緩衝區,這個緩衝區是存放readv所接收的資料或是writev將要發送的資料 size_t iov_len; //接收的長度以及實際寫入的長度}; count參數是vector數組的長度,即有多少塊記憶體資料需要從fd讀出或寫到fd。
兩者調用成功是返回讀出/寫入fd的位元組數,失敗返回-1,並設定errno。類似於簡化版的recvmsg和sendmsg。
以下簡陋的類比WEB伺服器,採用集中寫的方式。省略了HTTP請求的接收及解析, 直接將目標檔案作為第3個參數傳遞給服務端程式,用戶端telnet到服務端即可獲得該檔案。
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <assert.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <errno.h>#include <sys/stat.h>#include <sys/types.h>#include <fcntl.h>#define BUFFER_SIZE 1024//定義兩種HTTP狀態代碼和狀態資訊static const char *status_line[2] = {"200 OK", "500 Internal server error"};int main(int argc, char **argv){if(argc != 4) {fprintf(stderr, "Usage: %s ip port filename\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);const char *file_name = argv[3]; //將目標檔案作為程式的第三個參數傳入struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_port = htons(port);inet_pton(AF_INET, ip, &address.sin_addr);int sock = socket(PF_INET, SOCK_STREAM, 0);assert(sock >= 0);printf("create socket success\n");int reuse = 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));assert(ret != -1);fprintf(stderr, "bind address success\n");ret = listen(sock, 5);assert(ret != -1);fprintf(stderr, "listen success\n");struct sockaddr_in client;socklen_t client_addrlength = sizeof(client);fprintf(stderr, "start accept...\n");int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);if(connfd < 0) {printf("error: %s\n", strerror(errno));}else {char header_buf[BUFFER_SIZE]; //用於儲存HTTP應答的狀態行、頭部欄位和一個空的緩衝區memset(header_buf, '\0', BUFFER_SIZE);char *file_buf = NULL; //用於存放目標檔案內容的緩衝struct stat file_stat; //用於擷取目標檔案的屬性bool valid = true; //目標檔案是否有效int len = 0; //記錄header_buf當前已使用的位元組空間if(stat(file_name, &file_stat) < 0) { //目標檔案不存在valid = false;}else {if(S_ISDIR(file_stat.st_mode)) { //目標檔案是目錄valid = false;}else if (file_stat.st_mode & S_IROTH) { //目前使用者是否有讀許可權,相對於目標檔案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);fprintf(stderr, "reading %s file...", file_name);if (read(fd, file_buf, file_stat.st_size) < 0) {valid = false;}}else {valid = false;}}if (valid) { //目標檔案有效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: %ld\r\n",file_stat.st_size);len += ret;ret = snprintf(header_buf+len, BUFFER_SIZE-1-len,"%s", "\r\n"); //將header_buf和file_buf的內容一併寫出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;fprintf(stderr, "read %s success\nsending %s file to client...\n", file_name, file_name);ret = writev(connfd, iv, 2); //集中寫}else { //目標檔案無效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");fprintf(stderr, "read %s failed\nsending error message to client...\n", file_name);send(connfd, header_buf, strlen(header_buf), 0);}close(connfd);if (file_buf != NULL) {delete[] file_buf;file_buf = NULL;}}close(sock);return 0;}
sendfile函數
sendfile在兩個檔案描術符之間直接傳遞資料,完全在核心中操作,從而避免了核心緩衝區到使用者緩衝區的拷貝,因此效率很高,稱為零拷貝。
#include <sys/sendfile.h>ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd參數是待寫入內容的檔案描述符。
in_fd參數是待讀出內容的檔案描述符。
offset參數指定從讀入檔案流的哪個位置開始讀,如果為空白,則從檔案流的預設起始位置讀入。
count參數指定傳輸的位元組數。
調用成功時返回傳輸的位元組數,失敗則為-1,並設定errno
註: man手冊指出,in_fd必須是一個支援類似mmap函數的檔案描述符,
即它必須指向真實的檔案,不能是socket和管道;而out_fd必須是一個socket。可見,sendfile專為網路傳輸檔案而生。
//用sendfile函數傳輸檔案#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <assert.h>#include <errno.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <sys/types.h>#include <sys/stat.h>#include <sys/sendfile.h>#include <fcntl.h>int main(int argc, char **argv){if (argc <= 3) {fprintf(stderr, "Usage: %s ip port filename\n", basename(argv[0]));return 1;}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);struct stat stat_buf;fstat(filefd, &stat_buf);struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_port = htons(port);inet_pton(AF_INET, ip, &address.sin_addr);int sock = socket(PF_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) {fprintf(stderr, "errno is: %s\n", strerror(errno));}else {sendfile(connfd, filefd, NULL, stat_buf.st_size);close(connfd);}close(sock);return 0;}
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必須設定為NULL;否則off_in表示從輸入資料流的何處開始讀取,此時若為NULL,則從輸入資料流的當前位移位置讀入。
fd_out/off_out與上述相同,不過是用於輸出。
len參數指定移動資料的長度。
flags參數則控制資料如何移動:
- SPLICE_F_NONBLOCK:非阻塞的splice 操作,但實際效果還會受檔案描述符本身的阻塞狀態的影響。
- SPLICE_F_MORE:給核心一個提示:後續的 splice 系統調用將會有更多的資料傳。
- SPLICE_F_MOVE:如果合適的話,按整頁記憶體移動資料。這隻是給核心的一個提示。不過,因為它的實現存在BUG,所以自核心2.6.21後,它實際上沒有任何效果。
注意:使用splice時, fd_in和fd_out中必須至少有一個是管道檔案描述符。
調用成功時返回移動的位元組數量;它可能返回0,表示沒有資料需要移動,這通常發生在從管道中讀資料時而該管道沒有被寫入的時候。
失敗時返回-1,並設定errno。
splice函數可能產生的errno及其含義
| 錯誤 |
含義 |
| EBADF |
參數所指檔案描述符有錯 |
| EINVAL |
目標檔案系統不支援splice,或者目標檔案以追加方式開啟,或者兩個檔案描述符都不是管道檔案描述符,或者某個offset參數被用於不支援隨機訪問的裝置(比如字元裝置) |
| ENOMEM |
記憶體不夠 |
| ESPIPE |
參數fd_in(或fd_out)是管道檔案描述符,而off_in(或off_out)不為NULL |
下面代碼:通過splice將用戶端的內容讀入到管道中, 再從管道中讀出到用戶端,從而實現高效簡單的回顯服務。整個過程未執行recv/send,因此也未涉及使用者空間到核心空間的資料拷貝。
//使用splice實現的回顯伺服器#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <assert.h>#include <errno.h>#include <string.h>#include <fcntl.h>int main(int argc, char **argv){if (argc <= 2) {printf("usage: %s ip port\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;address.sin_port = htons(port);inet_pton(AF_INET, ip, &address.sin_addr);int sock = socket(PF_INET, SOCK_STREAM, 0);assert(sock >= 0);int reuse = 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));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: %s\n", strerror(errno));}else {int pipefd[2];ret = pipe(pipefd); //建立管道assert(ret != -1); //將connfd上的用戶端資料定向到管道中ret = splice(connfd, NULL, pipefd[1], NULL,32768, SPLICE_F_MORE | SPLICE_F_MOVE);assert(ret != -1); //將管道的輸出定向到connfd上ret = splice(pipefd[0], NULL, connfd, NULL,32768, SPLICE_F_MORE | SPLICE_F_MOVE);assert(ret != -1);close(connfd);}close(sock);return 0;}
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表示沒有複製任務資料。失敗時返回-1,並設定errno。
用tee和splice實現從標準輸入到終端和檔案的程式:
#include <stdio.h>#include <unistd.h>#include <string.h>#include <errno.h>#include <fcntl.h>#include <assert.h>int main(int argc, char **argv){if (argc != 2) {fprintf(stderr, "Usage: %s <file>\n", argv[0]);return 1;}uid_t uid = getuid();uid_t euid = geteuid();printf("userid: %d, effective userid: %d\n", uid, euid);int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);assert(filefd > 0);int pipefd_stdout[2];int ret = pipe(pipefd_stdout);assert(ret != -1);int pipefd_file[2];ret = pipe(pipefd_file);assert(ret != -1); //將標準輸入的內容輸出到管道ret = splice(STDOUT_FILENO, NULL, pipefd_stdout[1], NULL,32768, SPLICE_F_MORE | SPLICE_F_MOVE);assert(ret != -1); //將管道pipefd_stdout的輸出複製到管理pipefd_file的輸入端ret = tee(pipefd_stdout[0], pipefd_file[1],32768, SPLICE_F_NONBLOCK);assert(ret != -1); //將管道pipefd_file的輸出定向到檔案描述符filefd上,寫入檔案ret = splice(pipefd_file[0], NULL, filefd, NULL,32768, SPLICE_F_MORE | SPLICE_F_MOVE);assert(ret != -1); //將管道pipefd_stdout的輸出定向到標準出, 其內容和寫入檔案的內容一致ret = splice(pipefd_stdout[0], NULL, STDOUT_FILENO, NULL,32768, SPLICE_F_MORE | SPLICE_F_MOVE);assert(ret != -1);close(filefd);close(pipefd_file[0]);close(pipefd_file[1]);close(pipefd_stdout[0]);close(pipefd_stdout[1]);return 0;}