Linux高性能服务器编程——ch6笔记

第6章 高级I/O函数

6.1 pipe函数

用于创建一个管道,以实现进程间通信。

int pipe(int fd2);

读端文件描述符fd0和写端文件描述符fd1构成管道的两端,默认是阻塞的,fd0读出数据,fd1写入数据。管道内部传输的数据是字节流。

如果fd1的引用计数减少至0,即没有任何进程需要往管道中写入数据,则针对f0的read操作将返回0,即读取到了文件结束标记(EOF);反之,如果fd0计数减少至 0,即没有任何进程需要从管道读取数据,则针对fd1的write操作将失败,并引发SIGPIPE信号。

socketpair函数:双向管道,但仅能在本地使用。

6.2 dup函数和dup2函数

用于复制文件描述符,但不继承原文件描述符的属性。

int dup(int file_descriptor);

int dup2(int file_descriptor_one, int file_descriptor_two);

dup函数创建一个新的文件描述符(系统当前可用的最小整数),与原有file_descriptor指向相同文件、管道或者网络连接。

dup2函数类似,但返回第一个不小于file_descriptor_two的整数。

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[])
{
    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);  //关闭标准输出文件描述符,值为1
        dup(connfd);  //返回最小可用文件描述符,返回1
// 服务器输出到标准输出的内容就会直接发送到与客户连接对应的 socket 上
// 因此 printf 的输出将被客户端获得(而不是显示在服务器程序的终端上)
        printf("abcd\n");
        close(connfd);
    }

    close(sock);

    return 0;
}

6.3 readv函数和writev函数

ssize_t readv(int fd, const struct iovec* vector, int count);

ssize_t writev(int fd, const struct iovec* vector, int count);

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

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

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/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 = NULL;
        /* 用于获取目标文件的属性,比如是否为目录,文件大小等 */
        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, 并制定其大小为目标文件的大小
                 * file_stat.st_size 加1, 然后将目标文件读入缓存区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应答的状态行、"Content-Length"头部字段和一个空行
            * 依次加入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 = 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应答的发送。

6.4 sendfile函数

在内核中操作,在两个文件描述符之间直接传递数据。避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。

ssize_t sendlile(int out_fd, int in_fd, off_t* offset, size_t count);

in_fd(待读出)必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道;而out_fd(待写入)则必须是一个socket。由此可见,sendfile几乎是专门为在网络上传输文件而设计的。

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[])
{
    if (argc <= 3)
    {
        printf("usage: %s ip_address port_numberr 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(filefd);
    }

    close(sock);

    return 0;
}

没有为目标文件分配任何用户空间的缓存,也没有执行读取文件的操作,但同样实现 了文件的发送。

6.5 mmap函数和munmap函数

mmap函数用于申请一段内存空间。可以将这段内存作为进程间通信的共享(也可为调用进程所私有)内存,

也可以将文件内接映射到其中。munmap函数则释放由mmap创建的这段内存空间。

void* mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

int munmap(void *start, size_t length);

6.6 splice函数

用于在两个文件描述符之间移动数据,也是零拷贝。

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和fd_out必须至少有一个是管道文件描述符。

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]);

    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函数将客户端的内容读入到pipefd1中,然后再使用splice函数从pipefd0中读出该内容到客户端,从而实现了简单高效的回射服务。整个过程未执行recv或send操作,因此也未涉及用户空间和内核空间之间的数据拷贝。

6.7 tee函数

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

ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

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[])
{
    if (argc != 2)
    {
        printf("usage: %s <file>\n", basename(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函数

提供了对文件描述符的各种控制操作,另外一个常见的控制文件描述符属性和行为的系统调用是ioctl,而且ioctl比fcntl能够执行更多的控制。但是,对于控制文件描述符常用的属性和行为,fcntl函数是由POSIX规范指定的首选方法。

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

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

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux