《Linux高性能服务器编程》笔记02

Linux高性能服务器编程

参考

Linux高性能服务器编程源码: https://github.com/raichen/LinuxServerCodes

豆瓣: Linux高性能服务器编程

文章目录

  • Linux高性能服务器编程
    • 第06章 高级I/O函数
      • 6.1 pipe函数
      • 6.2 dup函数和dup2函数
      • 6.3 readv 函数和writev 函数
      • 6.4 sendfile 函数
      • 6.5 mmap 函数和munmap函数
      • 6.6 splice 函数
      • 6.7 tee函数
      • 6.8 fcntl函数
    • 后记

第06章 高级I/O函数

Linux提供了很多高级的I/O函数。它们并不像Linux基础I/O函数(比如open和read) 那么常用(编写内核模块时一般要实现这些I/O函数),但在特定的条件下却表现出优秀的性 能。本章将讨论其中和网络编程相关的几个,这些函数大致分为三类:

用于创建文件描述符的函数,包括pipe、dup/dup2函数。

用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee函数。

用于控制I/O行为和属性的函数,包括fcntl函数。

6.1 pipe函数

pipe函数可用于创建一个管道,以实现进程间通信。我们将在13.4节讨论如何使用管道 来实现进程间通信,本章只介绍其基本使用方式。pipe函数的定义如下:

#include <unistd.h>
int pipe( int fd[2] );

pipe函数的参数是一个包含两个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会有不同的行为。关于阻塞和非阻 塞的讨论,见第8章。如果管道的写端文件描述符fd[1]的引用计数(见5.7节)减少至0, 即没有任何进程需要往管道中写入数据,则针对该管道的读端文件描述符fd[0]的read操作 将返回0,即读取到了文件结束标记(End Of File,EOF);反之,如果管道的读端文件描述 符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文 件描述符fd[1]的write操作将失败,并引发SIGPIPE信号。关于SIGPIPE信号,我们将在 第10章讨论。

pipe 函数是用于创建管道的系统调用。管道是用于进程间通信的一种机制,它可以在两个进程之间传递数据。pipe 函数的声明如下:

#include <unistd.h>

int pipe(int fd[2]);
  • 参数

    • fd: 用于存储管道两端文件描述符的数组。fd[0] 是用于读取的文件描述符,fd[1] 是用于写入的文件描述符。
  • 返回值

    • 如果成功,返回 0;如果失败,返回 -1,并设置 errno

使用示例

#include <stdio.h>
#include <unistd.h>

int main() {
    int pipe_fd[2];

    // 创建管道
    if (pipe(pipe_fd) == -1) {
        perror("pipe");
        return 1;
    }

    // 管道创建成功,pipe_fd[0] 用于读取,pipe_fd[1] 用于写入

    // 关闭不需要的文件描述符
    close(pipe_fd[0]); // 关闭读取端
    close(pipe_fd[1]); // 关闭写入端

    return 0;
}

上述示例演示了如何使用 pipe 函数创建一个管道。创建成功后,pipe_fd[0] 用于读取,pipe_fd[1] 用于写入。通常,创建管道后,需要在进程中关闭不需要的文件描述符。

管道内部传输的数据是字节流,这和TCP字节流的概念相同。但二者又有细微的区别。应用层程序能往一个TCP连接中写入多少字节的数据,取决于对方的接收通告窗口的大小和 本端的拥塞窗口的大小。而管道本身拥有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少字节的数据。自Linux2.6.11内核起,管道容量的大 小默认是65536字节。我们可以使用fcntl函数来修改管道容量(见后文)。此外,socket的基础API中有一个socketpair 函数。它能够方便地创建双向管道。其定义如下:

#include<sys/types.h> 
#include<sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2] );

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

6.2 dup函数和dup2函数

有时我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接 (比如CGI编程)。这可以通过下面的用于复制文件描述符的dup或dup2函数来实现:

#include <unistd.h>
int dup( int flle_descriptor );
int dup2( int file_descriptor_one, int file_descriptor_two );

dup函数创建一个新的文件描述符,该新文件描述符和原有文件描述符file_descriptor指 向相同的文件、管道或者网络连接。并且dup返回的文件描述符总是取系统当前可用的最小 整数值。dup2和dup类似,不过它将返回第一个不小于file_descriptor_two的整数值。dup和 dup2系统调用失败时返回-1并设置errno。

6-1testdup.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);
        // 复制 connfd 到标准输出文件描述符的位置
        dup(connfd);
        // 此后标准输出将输出到 connfd 关联的套接字
        printf("abcd\n");
        // 关闭 connfd
        close(connfd);
    }

    // 关闭套接字
    close(sock);
    return 0;
}

这段代码的主要作用是创建一个服务器程序,监听指定端口,并在接收到客户端连接后将标准输出重定向到与客户端连接关联的套接字。

  • socket 创建:通过 socket 函数创建一个套接字,用于接收客户端连接。

  • bind 和 listen:使用 bind 绑定地址,然后通过 listen 监听连接。

  • accept:等待客户端连接,一旦有客户端连接,就会返回一个新的套接字 connfd

  • dup 函数:关闭标准输出文件描述符 (STDOUT_FILENO),然后使用 dup 函数将 connfd 复制到标准输出文件描述符的位置。这样,之后所有的 printf 输出都将写入到与客户端连接关联的套接字。

  • 输出到客户端:通过 printf 输出 “abcd”,这将通过与客户端连接的套接字发送给客户端。

  • 关闭套接字:关闭套接字,释放资源。

总体来说,这段代码演示了如何将标准输出重定向到与客户端连接的套接字,从而实现通过网络连接输出信息到客户端。

在代码清单6-1中,我们先关闭标准输出文件描述符STDOUT_FILENO(其值是1), 然后复制socket文件描述符connfd。因为dup总是返回系统中最小的可用文件描述符,所以 它的返回值实际上是1,即之前关闭的标准输出文件描述符的值。这样一来,服务器输出到 标准输出的内容(这里是“abcd”)就会直接发送到与客户连接对应的socket上,因此printf 调用的输出将被客户端获得(而不是显示在服务器程序的终端上)。这就是CGl服务器的基 本工作原理。

这段话描述了CGI服务器的基本工作原理。下面是对每个步骤的解释:

  1. 关闭标准输出文件描述符 (STDOUT_FILENO): 通过调用close(STDOUT_FILENO)关闭标准输出文件描述符。这是因为在CGI服务器的工作模式中,我们希望将动态生成的内容发送到与客户端连接相关联的套接字,而不是输出到服务器程序的终端。

  2. 复制socket文件描述符 (connfd): 使用dup(connfd)将套接字文件描述符 connfd 复制到系统中最小的可用文件描述符,而这个最小的可用文件描述符实际上就是关闭的标准输出文件描述符 STDOUT_FILENO 的值。这意味着现在套接字文件描述符 connfd 成为了标准输出文件描述符的副本。

  3. 输出到标准输出: 使用 printf 输出内容(在这里是 “abcd”)。由于标准输出文件描述符已经被复制为与客户端连接相关的套接字,所以 printf 的输出实际上会被发送到客户端而不是显示在服务器程序的终端上。

  4. 客户端接收: 因为标准输出已被重定向到与客户端连接的套接字,所以客户端将接收到服务器发送的 “abcd”。

总体而言,CGI服务器通过关闭标准输出,将套接字文件描述符复制到标准输出的位置,然后通过标准输出输出内容,实现了将动态生成的内容发送到与客户端连接相关的套接字,从而向客户端提供实时的动态内容。这是基本的CGI服务器工作原理。

6.3 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结构数组。我们在第5 章讨论过结构体iovec,该结构体描述一块内存区。count参数是vector数组的长度,即有多 少块内存数据需要从fd读出或写到fd。readv和writev在成功时返回读出/写入fd的字节数,失败则返回-1并设置errno。它们相当于简化版的recvmsg和sendmsg函数。

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

6-2testwritev.cpp

这段代码是一个简单的HTTP服务器,根据客户端请求的文件名,在响应中返回相应的文件内容。以下是对代码的注释和解释:

#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
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;
        int len = 0;
        
        // 检查文件状态
        if( stat( file_name, &file_stat ) < 0 )
        {
            valid = false;
        }
        else
        {
            // 检查是否为目录,是否有读权限
            if( S_ISDIR( file_stat.st_mode ) || !(file_stat.st_mode & S_IROTH) )
            {
                valid = false;
            }
            else
            {
                // 读取文件内容
                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;
                }
            }
        }
        
        if( valid )
        {
            // 构建HTTP响应头
            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" );
            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;
            
            // 使用 writev 函数将响应头和文件内容一并写入套接字
            ret = writev( connfd, iv, 2 );
        }
        else
        {
            // 发送500错误响应
            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响应。它能处理的请求包括:

  • 如果请求的文件存在且可读,返回一个包含文件内容的200 OK响应。
  • 如果请求的文件是目录或者不可读,返回一个500 Internal Server Error响应。

代码清单6-2中,我们省略了HTTP请求的接收及解析,因为现在关注的重点是HTTP 应答的发送。我们直接将目标文件作为第3个参数传递给服务器程序,客户telnet到该服务 器上即可获得该文件。关于HTTP请求的解析,我们将在第8章给出相关代码。

6.4 sendfile 函数

sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。sendfile函数的定义如下:

#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count ); 

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

以下是一个简单的使用 sendfile 函数的代码示例。该示例将一个文件的内容写入到套接字中。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <unistd.h>

int main() {
    // 打开源文件
    int in_fd = open("source.txt", O_RDONLY);
    if (in_fd == -1) {
        perror("Error opening source file");
        return 1;
    }

    // 创建套接字并绑定端口
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    // 省略套接字创建和绑定的代码

    // 打开目标文件(套接字)
    int out_fd = accept(sock, NULL, NULL);
    if (out_fd == -1) {
        perror("Error accepting connection");
        close(in_fd);
        close(sock);
        return 1;
    }

    // 获取源文件的大小
    struct stat stat_buf;
    fstat(in_fd, &stat_buf);

    // 使用 sendfile 将文件内容传输到套接字
    off_t offset = 0;
    ssize_t sent_bytes = sendfile(out_fd, in_fd, &offset, stat_buf.st_size);

    if (sent_bytes == -1) {
        perror("Error using sendfile");
    }

    // 关闭文件和套接字
    close(in_fd);
    close(out_fd);
    close(sock);

    return 0;
}

请注意,上述代码是一个简化的示例,实际应用中可能需要更多的错误检查和处理。

上述代码中的

struct stat stat_buf;
fstat(in_fd, &stat_buf);

解释如下:

struct stat stat_buf; 声明了一个结构体变量 stat_buf,该结构体用于存储文件的状态信息,包括文件大小、权限、最后访问时间等。fstat(in_fd, &stat_buf); 通过文件描述符 in_fd 获取文件状态信息,并将其保存在 stat_buf 中。

具体而言,fstat 函数的作用是获取与文件描述符相关联的文件的状态信息,并将这些信息填充到传入的结构体中。在这里,fstat 函数用于获取打开的源文件 in_fd 的状态信息,以便后续操作,如获取文件大小等。

struct stat 结构体的定义通常包含了很多字段,例如:

struct stat {
    dev_t         st_dev;      /* 文件所在设备的 ID */
    ino_t         st_ino;      /* 文件的 inode 号 */
    mode_t        st_mode;     /* 文件的类型和权限信息 */
    nlink_t       st_nlink;    /* 文件的硬链接数量 */
    uid_t         st_uid;      /* 文件的用户 ID */
    gid_t         st_gid;      /* 文件的组 ID */
    off_t         st_size;     /* 文件的大小(字节数)*/
    time_t        st_atime;    /* 最后访问时间 */
    time_t        st_mtime;    /* 最后修改时间 */
    time_t        st_ctime;    /* 最后状态改变时间 */
    blksize_t     st_blksize;  /* 文件系统 I/O 缓冲区大小 */
    blkcnt_t      st_blocks;   /* 分配给文件的块数量 */
};

在上述代码中,st_size 字段用于获取文件的大小(字节数),这对于确定文件的长度非常有用。

6-3testsendfile.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_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 );

    // 创建监听socket
    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发送文件内容
        sendfile( connfd, filefd, NULL, stat_buf.st_size );
        
        // 关闭连接
        close( connfd );
    }

    // 关闭监听socket
    close( sock );
    return 0;
}

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

6.5 mmap 函数和munmap函数

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

#include <sys/mman.h>
void* mmap( void *start, size_t length, int prot, int flags, int fd, off_t offset );
int munmap( void *start, size_t length );

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

PROT_READ,内存段可读。

PROT_WRITE,内存段可写。

PROT_EXEC,内存段可执行。

PROT NONE,内存段不能被访问。

flags参数控制内存段内容被修改后程序的行为。它可以被设置为表6-1中的某些值(这 里仅列出了常用的值)的按位或(其中MAP_SHARED和MAP_PRIVATE是互斥的,不能同时指定)。

在这里插入图片描述

fd参数是被映射文件对应的文件描述符。它一般通过open系统调用获得。offset参数设 置从文件的何处开始映射(对于不需要读入整个文件的情况)。mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED((void*)-1)并设置errno。munmap函数成功时返回0,失败则返回-1并设置errno。我们将在第13章进一步讨论如何利用mmap 函数实现进程间共享内存。

mmap 函数用于将一个文件或者其它对象映射到调用进程的地址空间,而 munmap 函数用于解除这种映射关系。

  • mmap 函数参数解释:

    • start: 指定映射的起始地址,通常设置为0,由系统自动分配。
    • length: 映射的长度。
    • prot: 保护标志,指定映射区的保护方式,可以是PROT_NONE(不可访问),PROT_READ(可读),PROT_WRITE(可写),PROT_EXEC(可执行)等。
    • flags: 映射区的类型和映射对象的处理方式,可以是MAP_SHARED(共享映射)或MAP_PRIVATE(私有映射)等。
    • fd: 文件描述符,映射的文件。
    • offset: 文件映射的起始位置。
  • munmap 函数参数解释:

    • start: 映射区的起始地址。
    • length: 映射区的长度。

以下是一个简单的代码示例:

#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char* file_path = "example.txt";
    const size_t file_size = 4096;

    // 打开文件
    int fd = open(file_path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 调整文件大小
    if (ftruncate(fd, file_size) == -1) {
        perror("ftruncate");
        close(fd);
        return 1;
    }

    // 映射文件到内存
    void* mapped_data = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped_data == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 将数据写入映射区
    const char* message = "Hello, Memory-mapped File!";
    strncpy(mapped_data, message, strlen(message));

    // 解除映射关系
    if (munmap(mapped_data, file_size) == -1) {
        perror("munmap");
    }

    // 关闭文件
    close(fd);

    return 0;
}

此示例创建了一个文件,通过 mmap 将文件映射到内存中,然后写入数据,最后通过 munmap 解除映射关系。

6.6 splice 函数

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参数是待输入数据的文件描述符。如果fd_in是一个管道文件描述符,那么off_in 参数必须被设置为NULL。如果fd_in不是一个管道文件描述符(比如socket),那么off_in表示从输入数据流的何处开始读取数据。此时,若off_in被设置为NULL,则表示从输入数据流的当前偏移位置读入;若off_in不为NULL,则它将指出具体的偏移位置。fd_out/off_ out参数的含义与fd_in/off_in相同,不过用于输出数据流。len参数指定移动数据的长度; flags参数则控制数据如何移动,它可以被设置为表6-2中的某些值的按位或。

在这里插入图片描述

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

在这里插入图片描述

下面我们使用splice函数来实现一个零拷贝的回射服务器,它将客户端发送的数据原样 返回给客户端,具体实现如代码清单6-4所示。

6-4testsplice.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 );

        // 从套接字读取数据并通过 splice 复制到管道
        ret = splice( connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); 
        assert( ret != -1 );

        // 从管道读取数据并通过 splice 复制到套接字
        ret = splice( pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
        assert( ret != -1 );

        // 关闭连接套接字
        close( connfd );
    }

    // 关闭监听套接字
    close( sock );
    return 0;
}

注释和解释

  1. 创建套接字、绑定、监听:通过 socketbindlisten 创建并设置服务器套接字。

  2. 接受客户端连接:使用 accept 函数等待客户端连接,得到连接套接字 connfd

  3. 创建管道:使用 pipe 创建一个管道,pipefd[0] 是读取端,pipefd[1] 是写入端。

  4. 通过 splice 实现数据传输:使用两次 splice 函数,第一次从连接套接字 connfd 中读取数据并写入管道,第二次从管道读取数据并写入连接套接字。这样实现了零拷贝,避免了数据在用户空间和内核空间之间的复制。

  5. 关闭连接套接字:关闭已经处理完的连接套接字。

  6. 关闭监听套接字:关闭服务器监听套接字。

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

6.7 tee函数

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 必须都是管道文件描述符)。tee 函数成功时返回在两个文件描述符之间复制的数据数量(字节数)。返回0表示没有复制任 何数据。tee失败时返回-1并设置errno。

代码清单6-5利用tee 函数和splice函数,实现了Linux下tee程序(同时输出数据到终 端和文件的程序,不要和tee函数混淆)的基本功能。

6-5testtee.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", 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 );

    // 使用 splice 将标准输入的内容写入 pipefd_stdout[1] 管道
    ret = splice( STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
    assert( ret != -1 );

    // 使用 tee 函数将 pipefd_stdout[0] 管道的内容同时写入 pipefd_file[1] 管道和标准输出
    ret = tee( pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK );
    assert( ret != -1 );

    // 使用 splice 将 pipefd_file[0] 管道的内容写入文件
    ret = splice( pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
    assert( ret != -1 );

    // 使用 splice 将 pipefd_stdout[0] 管道的内容写入标准输出
    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;
}

作用: 该程序通过 splicetee 函数实现了将标准输入的内容同时写入文件和标准输出的功能。使用管道和文件描述符传输数据,无需用户空间和内核空间之间的数据拷贝,从而提高了效率。

6.8 fcntl函数

fcntl 函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制操 作。另外一个常见的控制文件描述符属性和行为的系统调用是ioctl,而且ioctl比fcntl能够 执行更多的控制。但是,对于控制文件描述符常用的属性和行为,fcntl函数是由POSIX规 范指定的首选方法。所以本书仅讨论fcntl 函数。fcntl函数的定义如下:

#include <fcntl.h>
int fcntl( int fd, int cmd,);

fd参数是被操作的文件描述符,cmd参数指定执行何种类型的操作。根据操作类型的不 同,该函数可能还需要第三个可选参数arg。

#include <fcntl.h>

int fcntl( int fd, int cmd, ... );

解释:

  • fd 文件描述符,是需要进行操作的文件或套接字的标识符。
  • cmd 控制命令,指定对文件描述符 fd 进行的操作。

fcntl 函数用于对文件描述符进行各种控制操作,取决于 cmd 参数的值。该函数的第三个参数 arg 的具体含义取决于 cmd 的值。

常见的 cmd 可选值:

  1. F_DUPFD 复制文件描述符。arg 为新的文件描述符的最小允许值。
  2. F_GETFL 获取文件状态标志。arg 为无符号整数,表示文件的状态标志。
  3. F_SETFL 设置文件状态标志。arg 为要设置的状态标志的位掩码。
  4. F_GETLK 获取文件锁信息。arg 为指向 struct flock 结构的指针,用于存储锁信息。
  5. F_SETLK 设置文件锁。arg 为指向 struct flock 结构的指针,用于设置锁信息。
  6. F_SETLKW 设置文件锁,如果无法获取锁则阻塞。arg 为指向 struct flock 结构的指针。

示例:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd = open("example.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 获取文件状态标志
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl");
        close(fd);
        return 1;
    }

    // 设置文件状态标志,添加 O_APPEND 标志
    flags |= O_APPEND;
    int result = fcntl(fd, F_SETFL, flags);
    if (result == -1) {
        perror("fcntl");
        close(fd);
        return 1;
    }

    // 其他操作...

    close(fd);
    return 0;
}

上述示例中,通过 fcntl 函数获取文件的状态标志,然后设置了 O_APPEND 标志,将文件设置为以追加方式打开。

后记

截至2024年1月20日11点21分,学习完《Linux高性能服务器编程》第六章的内容,主要介绍Linux的基础I/O函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/335913.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于SSM的KTV包厢管理系统(有报告)。Javaee项目,ssm项目。

演示视频&#xff1a; 基于SSM的KTV包厢管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过…

软件测试|sqlalchemy一对一关系详解

简介 SQLAlchemy 是一个强大的 Python ORM&#xff08;对象关系映射&#xff09;库&#xff0c;它允许我们将数据库表映射到 Python 对象&#xff0c;并提供了丰富的关系模型来处理不同类型的关系&#xff0c;包括一对一关系。在本文中&#xff0c;我们将深入探讨 SQLAlchemy …

后台管理系统: 数据可视化基础

数据可视化简单理解&#xff0c;就是将数据转换成易于人员辨识和理解的视觉表现形式&#xff0c;如各种 2D 图表、3D 图表、地图、矢量图等等。 例如Excel等等 canvas <canvas> 标签只是图形容器&#xff0c;相当于一个画布&#xff0c;canvas 元素本身是没有绘图能力…

算法常用思路总结

思路 1. 求数组中最大最小值思路代码 2. 计算阶乘思路&#xff1a;代码&#xff1a; 3. 得到数字的每一位思路代码 4. 计算时间类型5. 最大公约数、最小公倍数6. 循环数组的思想题目&#xff1a;猴子选大王代码 补充经典例题1. 复试四则运算题目内容题解 2. 数列求和题目内容题…

安防监控系统EasyCVR平台用户调用设备参数,信息不返回是什么原因?

安防视频监控系统EasyCVR视频综合管理平台&#xff0c;采用了开放式的网络结构&#xff0c;平台能在复杂的网络环境中&#xff08;专网、局域网、广域网、VPN、公网等&#xff09;将前端海量的设备进行统一集中接入与视频汇聚管理&#xff0c;平台支持设备通过4G、5G、WIFI、有…

限流算法之流量控制的平滑之道:滑动时间窗算法

文章目录 引言简介优点缺点样例样例图样例代码 应用场景结论 引言 在互联网应用中&#xff0c;流量控制是一个重要的组件&#xff0c;用于防止系统过载和保护核心资源。常见的限流算法包括固定窗口算法和滑动时间窗算法。本文将重点介绍滑动时间窗算法&#xff0c;并分析其优缺…

掌握虚拟化:PVE平台安装教程与技术解析

&#x1f31f;&#x1f30c; 欢迎来到知识与创意的殿堂 — 远见阁小民的世界&#xff01;&#x1f680; &#x1f31f;&#x1f9ed; 在这里&#xff0c;我们一起探索技术的奥秘&#xff0c;一起在知识的海洋中遨游。 &#x1f31f;&#x1f9ed; 在这里&#xff0c;每个错误都…

Windows系统下使用docker-compose安装mysql8和mysql5.7

windows环境搭建专栏&#x1f517;点击跳转 win系统环境搭建&#xff08;十四&#xff09;——Windows系统下使用docker安装mysql8和mysql5.7 文章目录 win系统环境搭建&#xff08;十四&#xff09;——Windows系统下使用docker安装mysql8和mysql5.7MySQL81.新建文件夹2.创建…

结构体的使用和结构体指针的定义注意事项

1、使用背景 由于想把不同地方的三个变量数据存放在一个结构体中&#xff0c;并且调用W25QXX_Write((u8*)p,FLASH_SIZE-100,SIZE); //从倒数第100个地址处开始,写入SIZE长度的数据。调用flash写数据函数&#xff0c;其参数为指针地址&#xff0c;于是需要定义一个结构体和指向结…

最小二乘法拟合二维点

方法1&#xff1a;使用np.polyfit()函数进行拟合 import numpy as np import matplotlib.pyplot as plt# 模拟数据 x np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) y np.array([1, 3, 2, 4, 7, 10, 11, 15, 17, 20])# 使用多项式拟合&#xff0c;这里选择二次多项式 coefficie…

C++——函数的定义

1&#xff0c;概述 作用&#xff1a;将一段经常使用的代码封装起来&#xff0c;减少重复代码 一个较大的程序&#xff0c;一般分为若干个程序块&#xff0c;每个模块实现特定的功能。 2&#xff0c;函数的定义 函数的定义一般主要有五个步骤&#xff1a; 1&#xff0c;返回…

macOS修改默认时区显示中国时间

默认时区不是中国,显示时间不是中国时间 打开终端 ,删除旧区,并复制新时区到etcreb sudo -rm -rf /etc/localtime sudo ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 重启系统后时间显示为中国时间

日志记录logging

文章目录 1. logging基础使用1.1 日志的6个级别1.2 logging.basicConfig1.3 案例 2. logging的高级应用2.1 记录器Logger2.2 处理器- Handler2.3 格式器- Formatter2.4 创建关联2.4 案例 3.在项目中的应用3.1 定义全局使用的logger对象3.2 使用案例 参考 1. logging基础使用 1…

【Linux】配置dns主从服务器,能够实现正常的正反向解析

​​​​​​1、首先&#xff0c;在主服务器上配置DNS解析器。打开配置文件/etc/named.conf&#xff0c;添加以下内容&#xff1a; zone"example.com" IN {type master;file "example.com.zone";allow-transfer { slave_ip_address: }; };zone"xx.16…

减少 LLM 幻觉方法--CoVe

​来自于 Meta AI&#xff0c;原文链接&#xff1a;https://arxiv.org/abs/2309.11495 LLM 经常遇到的主要问题就是幻觉&#xff0c;减少幻觉的方法大致可分为三类&#xff1a;训练时校正、生成时校正和通过增强&#xff08;使用工具&#xff09;进行校正。 在训练时校正的方法…

burp靶场--文件上传

burp靶场–文件上传 https://portswigger.net/web-security/file-upload/lab-file-upload-remote-code-execution-via-web-shell-upload 1.文件上传 1、原理&#xff1a;文件上传漏洞是指Web服务器允许用户将文件上传到其文件系统&#xff0c;而不充分验证文件的名称、类型、…

SpringMVC环境搭配

概述 Spring MVC是Spring Framework提供的Web组件&#xff0c;全称是Spring Web MVC,是目前主流的实现MVC设计模式的框架&#xff0c;提供前端路由映射、视图解析等功能 mvc是什么 MVC是一种软件架构思想&#xff0c;把软件按照模型&#xff0c;视图&#xff0c;控制器来划分…

linux基础学习(5):yum

yum是为了解决rpm包安装依赖性而产生的一种安装工具 1.yum源 1.1配置文件位置 yum源的配置文件在/etc/yum.repos.d/中 *Base源是网络yum源&#xff0c;也就是需要联网才能使用的yum源。默认情况下&#xff0c;系统会使用Base源 *Media源是光盘yum源&#xff0c;是本地yum源…

如何防止你的 Goroutine 泄露 Part2

文章目录 简述NumGoroutine演示案例pprofruntime/pprofhttp/net/pprof gopsLeak Test总结参考资料 上篇 文章说到&#xff0c;防止 goroutine 泄露可从两个角度出发&#xff0c;分别是代码层面的预防与运行层面的监控检测。今天&#xff0c;我们来谈第二点。 简述 前文已经介…

GPT应用开发:编写插件获取实时天气信息

欢迎阅读本系列文章&#xff01;我将带你一起探索如何利用OpenAI API开发GPT应用。无论你是编程新手还是资深开发者&#xff0c;都能在这里获得灵感和收获。 本文&#xff0c;我们将继续展示聊天API中插件的使用方法&#xff0c;让你能够轻松驾驭这个强大的工具。 插件运行效…