高级I/O函数
6.1 pipe函数
pipe函数可用于创建一个管道,以实现进程间通信。其定义如下:
#include <unistd.h>
int pipe( int fd[2] );
// 函数成功时返回0,并将一对打开的文件描述符值填入其参数指向的数组
// 函数失败返回-1,并设置errno
- pipe函数创建的两个文件描述符fd[0]和fd[1]分别构成管道的两端,往fd[1]写入的数据可以从fd[0]读出,且fd[0]只能从管道读出数据,fd[1]只能往管道写入数据。若想要实现双向的数据传输,应该使用两个管道。
- 默认情况下,这一对文件描述符都是阻塞的。
- 管道内部传输的数据是字节流,其本身拥有一个容量限制,其大小默认是65536字节,可使用fcntl函数来修改管道容量。
- 双向管道创建可使用sockpair函数:
#include <sys/types.h>
#include <sys/socket.h>
// domain只能使用UNIX本地协议族AF_UNIX
int socketpair(int domain, int type, int protocol, int fd[2]);
// 创建成功返回0,失败返回-1并设置errno
dup函数和dup2函数
当我们希望把标准输入重定向到一个文件,或定向到一个网络连接,可通过复制文件描述符的dup和dup2来实现:
#include <unistd.h>
// 创建一个新的文件描述符,和原有文件描述符指向相同的文件、管道或网络连接
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
// 调用失败返回-1,并设置errno
实例:利用dup函数实现一个基本的CGI服务器
#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[]){
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(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("errno is : %d\n", errno);
}
else{
// 关闭标注文件描述符
close(STDOUT_FILENO);
dup(connfd);
printf("abcd\n");
close(connfd);
}
close(sock);
return 0;
}
6.3 readv函数和writev函数
readv函数将数据从文件描述符读到分块的内存中,即分散读;writev函数将多块分散的内存一并写入文件描述符中,即集中写。
#include <sys/uio.h>
// fd是被操作的目标文件描述符,vector是iovec结构数组,count是vector数组长度
ssize_t readv(int fd, const struct iovec* vector, int count);
ssize_t writev(int fd, const struct iover* vector, int count);
// 成功时返回读出或写入的fd字节数,失败返回-1并设置errno
实例:Web服务器上的集中写
#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
// 定义两种HTTP状态码和状态信息
static const char* status_line[2] = {"200 OK", "500 Internal server error"};
int main(int argc, char* argv[]){
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];
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);
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应答的状态行、头部字段和一个空行缓存区
char header_buf[BUFFER_SIZE];
memset(header_buf, '\0', BUFFER_SIZE);
// 用于存放目标文件内容的应用程序缓存
char* file_buf;
// 用于获取目标文件的属性,如目录、大小等
struct stat file_stat;
// 记录目标文件是否是有效文件
bool valid = true;
// 缓存区header_buf目前已经使用了多少字节空间
int len = 0;
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){ //当前用户有读取目标文件的权限
// 动态分配缓存区file_buf
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;
}
}
else{
valid = false;
}
}
// 若目标文件有效,发送正常HTTP应答
if(valid){
// 将HTTP应答的状态行、头部字段、空行依次加入header_buf中
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");
// 利用writev将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;
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 = snprinrf(header_buf+len, BUFFER_SIZE-1-len, "%s", "\r\n");
send(connfd, header_buf, strlen(header_buf), 0);
}
close(connfd);
delete [] file_buf;
}
close(sock);
return 0;
}
6.4 sendfile函数
sendfile函数在两个文件描述符之间传递数据,其完全在内核中操作,避免了内核缓冲区和用户缓冲区的数据拷贝,被称为零拷贝。
#include <sys/senfile.h>
// in_fd:待读出内容文件描述符;out_fd:待写入内容文件描述符
// offset:指定从读入文件流那个位置开始读,默认未起始位置
// count:in_fd和out_fd之间传输的字节数
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
// 成功是返回传输的字节数,失败返回-1并设置额errno
实例:利用sendfile函数传输文件
#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/senfile.h>
int main(int argc, char* argv[]){
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];
int filefd = open(file_name, O_RDONLY);
assert(fildfd>0);
struct stat stat_buf;
fstat(filefd, &stat_buf);
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);
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(connfd, filefd, NULL, stat_buf.st_size);
close(connfd);
}
close(sock);
return 0;
}
6.5 mmap函数和munmap函数
mmap函数用于申请一段内存空间,可将其作为进程间通信的共享内存,也可将文件直接映射到其中。munmap函数释放mmap创建的内存空间
#include <sys/mman.h>
// start:某个特定地址作为起始地址,若为NULL则自动分配
// length:指定内存段的长度
// prot:设置内存段的访问权限:PROT_READ(内存段可读)、PROT_WRITE(内存段可写)、PROT_EXEC(内存段可执行)、PROT_NONO(内存段不可被访问)
// flags参数控制内存段内容被修改后的行为,如调为私有、共享等
// fd参数是被映射文件对应的文件描述符,通过open系统调用获得
// offset设置从文件何处开始映射
void* mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
// 成功返回指向目标内存区域的指针,失败返回MAP_FAILED((void*)-1)并设置errno
int munmap(void *start, size_t length);
// 成功返回0,失败返回-1并设置errno
6.6 splice函数
splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。
#include <fcntl.h>
// fd_in:待输入数据的文件描述符;
// off_in:当fd_in是管道文件描述符时,必须设为NULL;否则设置从何处开始读取数据,为NULL表示从当前偏移位置读取;
// len:移动数据的长度
// flags:控制数据如何移动(SPLICE_F_MOVE, SPLICE_F_NONBLOCK, SPLICE_F_MORE, SPLICE_F_GIFT)
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);
实例:使用splice函数实现的回射映射器
#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]);
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);
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{
int pipefd[2];
assert(ret!=-1);
ret = pipe(pipefd); // 创建管道
// 将connfd上流入客户都数据定向到管道中
ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE|SPLICE_F_MOVE);
assert(ret!=-1);
ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE|SPLICE_F_MOVE);
assert(ret!=-1);
close(connfd);
}
close(sock);
return 0;
}
6.7 tee函数
tee函数在两个管道文件描述符之间复制数据,也是零零拷贝操作。
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
实例:同时输出数据到终端和文件的程序
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <funtl.h>
int main(int argc, char* argv[]){
if(argc!=2){
printf("usage: %s <file>\n",argv[0]);
return 1;
}
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);
// 将标注输入内容输入管道pipefd_stdout
ret = splice(STDIN_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_stdout[0]);
close(pipefd_stdout[1]);
close(pipefd_file[0]);
close(pipefd_file[1]);
return 0;
}
6.8 fcntl函数
fcntl函数提供了对文件描述符的控制操作。
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
// 将文件描述设置为阻塞
int setnonblocking(int fd){
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}