【阅读】《Linux高性能服务器编程》——第六章:高级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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

甄姬、巴豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值