Linux高性能服务器编程 学习笔记 第六章 高级IO函数

pipe函数用于创建一个管道,以实现进程间通信:

fd参数是一个包含两个int的数组。该函数成功时返回0,并将一对打开的文件描述符填入其参数指向的数组,如果失败,则返回-1并设置errno。

pipe函数创建的这两个文件描述符fd[0]和fd[1]分别构成管道的两端,往fd[1]写入的数组可以从fd[0]读出,并且fd[0]只能用于从管道读出数据,fd[1]只能用于往管道写入数据,不能反过来使用。如果要实现双向的数据传输,就应该使用两个管道。默认,这一对文件描述符都是阻塞的。如果我们用read系统调用读取一个空管道,则read函数将被阻塞,直到管道内有数据可读;如果我们用write系统调用往一个满的管道中写入数据,则write函数也被阻塞,直到管道有足够的空闲空间可用。如果应用进程将fd[0]和fd[1]都设为非阻塞的,则read和write函数会有不同行为。如果管道写端文件描述符fd[1]的引用计数减少到0,即没有任何进程需要往管道中写入数据,则针对该管道的读端文件描述符fd[0]的read操作将返回0,即读取到文件结束标记(EOF,End Of File);反之,如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符fd[1]的write操作将失败,并引发SIGPIPE信号。

管道内部传输的数据是字节流,这和TCP字节流的概念相同。应用层进程能往一个TCP连接中写入多少字节的数据,取决于双方的接收通告窗口的大小和本端的拥塞窗口的大小,而管道本身拥有一个容量限制,它规定如果应用进程不将数据从管道读走,该管道最多能被写入多少字节数据。自Linux 2.6.11内核起,管道容量的大小默认是65535字节,我们可用fcntl函数修改管道容量。

socket的基础API中有一个socketpair函数,它能创建双向管道:

socketpair函数的前3个参数的含义与socket系统调用的前3个参数完全相同,但domain参数只能使用UNIX本地域协议族AF_UNIX,因为我们仅能在本地使用这个双向管道。fd参数和pipe系统调用的参数一样,但socketpair函数创建的这对文件描述符都是既可读又可写的。socketpair函数成功时返回0,失败时返回-1并设置errno。

有时我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接(比如CGI(Common Gateway Interface)编程,它一种用于创建交互式网络应用程序的技术,它是一种在Web服务器和其他计算机程序之间进行通信的标准方法,允许Web服务器调用外部程序来处理Web请求,并将结果发送回浏览器),这可通过以下用于复制文件描述符的dup或dup2函数来实现:

dup函数创建一个新文件描述符,该新文件描述符和原有文件描述符file_descriptor参数指向相同的文件、管道、网络连接,且dup函数返回的文件描述符总是取系统当前可用的最小整数值。dup2和dup函数类似,但它将新文件描述符设置为file_descriptor_two参数,如果新文件描述符之前已经打开,那么在重用之前,它将被关闭,关闭操作是静默的(即dup2函数不会报告关闭过程中的任何错误)。关闭和重用新文件描述符的步骤是原子的,这一点很重要,因为试图使用close函数和dup函数实现等效功能将会受到竞态条件的影响,在两个步骤之间,newfd可能会被重用,这种重用可能是因为主程序被分配了一个文件描述符的信号处理器中断,或者是因为并行线程分配了一个文件描述符。dup和dup2系统调用失败时返回-1并设置errno。

通过dup和dup2函数创建的文件描述符不继承原文件描述符的属性,如close-on-exec和non-blocking等。

以下程序使用dup函数实现了一个基本的CGI服务器:

c 复制代码
#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 <libgen.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 {
        // 先关闭标准输出文件描述符STDOUT_FILENO,其值为1
        close(STDOUT_FILENO);
        // 复制socket文件描述符connfd,由于dup函数总是返回系统中最小的可用文件描述符
        // 因此dup参数实际返回的是1,即之前关闭的标准输出文件描述符的值
        // 这样服务器输出到标准输出的内容会直接发送到与客户连接对应的socket上
        dup(connfd);
        printf("abcd\n");
        close(connfd);
    }

    close(sock);
    return 0;
}

readv函数将数据从文件描述符读到分散的内存块中,即分散读;writev函数将多块分散的内存数据一并写入文件描述符中,即集中写:

fd参数是被操作的目标文件描述符。vector参数的类型是iove结构数组,该结构体描述一块内存区。count参数是vector数组的长度。readv和writev函数在成功时返回读出/写入fd的字节数,失败则返回-1并设置errno。

当Web服务器解析完一个HTTP请求后,如果目标文档存在且客户具有读取该文档的权限,则服务器就需要发送一个HTTP应答来传输该文档,这个HTTP应答包括1个状态行、多个头部字段、一个空行和文档内容,其中前3部分内容可能被Web服务器放置在一块内存中,而文档内容则通常被读入到另一块单独的内存中(通过read或mmap函数),我们不需要手动把这两部分内容拼接到一起再发,而是可以使用writev函数将它们同时写出,如下代码所示:

c 复制代码
#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>
#include <libgen.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) {
                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) {
            // snprintf函数返回格式化的字符串的长度
            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 = snprintf(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;
}

以上代码中,我们省略了HTTP请求的接收和解析,因为我们关心的重点是HTTP应答的发送。我们直接将目标文件作为第3个参数传递给服务器程序,客户telnet到该服务器上即可获得该文件。

sendfile函数在两个描述符之间直接传递数据(完全在内核中操作),例如从文件到网络套接字,从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这称为零拷贝(零拷贝旨在减少数据在内存之间复制的次数,有时人们会将零拷贝保留用于描述在完全不涉及数据复制的情况下的数据传输,sendfile函数虽然减少了用户空间的数据拷贝,但仍然存在内核空间的数据传输,因此有些人可能认为它是半零拷贝(Partial Zero Copy))。sendfile函数定义:

in_fd参数是待读出内容的文件描述符。out_fd参数是待写入内容的文件描述符。offset参数指定从读入文件流的哪个位置开始读,如果为NULL,则使用读入文件流默认的起始位置。count参数指定在文件描述符in_fd参数和out_fd参数之间传输的字节数。sendfile函数成功时返回传输的字节数,失败则返回-1并设置errno。sendfile函数的man手册明确指出,in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket或管道,而out_fd参数则必须是一个socket,由此可见,sendfile函数几乎是专门为在网络上传输文件而设计的。以下程序利用sendfile函数将服务器上的一个文件传送给客户:

c 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <libgen.h>
#include <stdlib.h>
#include <unistd.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(filefd > 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;
}

以上代码中,我们将目标文件作为第3个参数传递给服务器进程,客户telnet到该服务器上即可获得该文件。相比以上使用writev函数发送文件的程序,以上代码没有为目标文件分配任何用户空间缓存,也没有执行读取文件的操作,但同样实现了文件发送,效率要高很多。

mmap函数用于申请一段内存空间,我们可将这段内存作为进程间通信的共享内存,也可将文件直接映射到其中。munmap函数释放由mmap函数创建的这段内存:

start参数允许用户使用某个特定的地址作为这段内存的起始地址,如果它是NULL,则系统自动分配一个地址。length参数指定内存段的长度。prot参数设置内存段的访问权限,它可以是以下几个值的按位或:

1.PROT_READ,内存段可读。

2.PROT_WRITE,内存段可写。

3.PROT_EXEC,内存段可执行。

4.PROT_NONE,内存段不能被访问。

flags参数控制内存段内容被修改后程序的行为,它可被设置为下表中的某些值(只列出了常用的值)的按位或(其中MAP_SHARED和MAP_PRIVATE是互斥的):

fd参数是被映射文件对应的文件描述符,它一般通过open系统调用获得。offset参数设置从文件的何处开始映射(对于不需要读入整个文件的情况)。

mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED并设置errno。munmap函数成功时返回0,失败则返回-1并设置errno。

splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作:

fd_in参数是待输入数据的文件描述符,如果fd_in是一个管道文件描述符,那么off_in参数必须被设置为NULL,否则,off_in参数表示从输入数据流的何处开始读取数据,此时,若off_in被设置为NULL,则表示从输入数据流的当前偏移位置读入,若off_in不为NULL,则它指出具体的偏移位置。fd_out/off_out参数的含义与fd_in/off_in参数相同,不过用于输出数据流。len参数指定移动数据的长度。flags参数控制数据如何移动,它可被设为下表值的按位或:

SPLICE_F_GIFT标志是用于vmsplice函数(一个Linux系统调用,用于在用户空间和内核空间之间传输数据,通常用于高性能的数据传输操作)的。

使用splice函数时,fd_in和fd_out必须至少有一个是管道描述符。splice函数调用成功时返回移动字节的数量,它可能返回0,表示没有数据需要移动,这发生在从管道中读取数据(此时fd_in是管道文件描述符),而该管道中没有任何数据时。splice函数失败时返回-1并设置errno,常见的errno如下表:

下面使用splice函数实现一个零拷贝的回射服务器,它将客户端发送的数据原样返回给客户端:

c 复制代码
#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>
#include <libgen.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];
        // 创建管道
        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;
}

以上代码通过splice函数将客户端的内容读入pipefd[1]中,然后再使用splice函数从pipefd[0]中读出该内容到客户端,从而实现了简单高效的回射服务,整个过程未执行recv/send函数,因此也未涉及用户空间和内核空间之间的数据拷贝。

tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作,它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作:

该函数的参数含义与splice函数的参数相同(但fd_in和fd_out必须都是管道文件描述符)。tee函数成功时返回在两个文件描述符之间复制的数据字节数,返回0表示没有复制任何数据。tee函数失败时返回-1并设置errno。

以下程序利用splice和tee函数,实现了Linux下tee程序(同时输出数据到终端和文件)的基本功能:

c 复制代码
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.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的输出重定向到文件描述符fd上,从而将标准输入的内容写入文件
    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;
}

fcntl函数正如其名字file control描述的那样,提供了对文件描述符的各种控制,另一个常见的控制文件描述符属性和行为的系统调用是ioctl,且ioctl函数比fcntl函数能执行更多的控制,但控制文件描述符的常用属性和操作,fcntl函数是POSIX规范指定的首选方法:

fd参数是被操作的文件描述符,cmd参数指定执行何种类型的操作,根据操作类型的不同,该函数可能还需要第三个可选参数。fcntl函数支持的常用操作及其参数见下表:

fcntl函数成功时返回值见上表最后一列,失败时返回-1并设置errno。

网络编程中,fcntl函数通常用来将一个文件描述符设为非阻塞的:

c 复制代码
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;    // 返回旧的状态标志,以便日后恢复该状态标志
}

SIGIO和SIGURG这两个信号与其他Linux信号不同,它们必须与某个文件描述符相关联才能使用。当被关联的文件描述符可读或可写时,系统将触发SIGIO信号;当被关联的文件描述符(必须是一个socket)上有带外数据可读时,系统将触发SIGURG信号。将信号和文件描述符关联的方法是使用fcntl函数为目标文件描述符指定宿主进程或进程组,则被指定的宿主进程或进程组将捕获到这两个信号。使用SIGIO时,还需要用fcntl函数设置套接字描述符的O_ASYNC标志(异步IO标志,但SIGIO信号模型并非真正意义上的异步IO模型)。

相关推荐
ragnwang几秒前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
Jackey_Song_Odd40 分钟前
解决Ubuntu下无法装载 Windows D盘的问题
linux·ubuntu
乔巴不是狸猫1 小时前
第11周作业
linux
Web阿成2 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript
雷神乐乐2 小时前
Spring学习(一)——Sping-XML
java·学习·spring
李雨非-19期-河北工职大2 小时前
思考: 与人交际
学习
Bessssss2 小时前
centos权限大集合,覆盖多种权限类型,解惑权限后有“. + t s”问题!
linux·运维·centos
哦哦~9212 小时前
深度学习驱动的油气开发技术与应用
大数据·人工智能·深度学习·学习
silver6873 小时前
Linux 下的 GPT 和 MBR 分区表详解
linux