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

news2024/11/14 13:38:22

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函数。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redmine替代品合集:6款值得尝试的项目管理软件

在软件开发和项目管理行业&#xff0c;Redmine曾经是一个大家公认的、基于Web的项目管理工具。随着技术的发展和团队需求的多样化&#xff0c;市场上出现了许多功能强大的Redmine替代品。这些工具在特定方面—无论是用户界面、功能集、集成能力还是用户体验上—都可能提供更新颖…

Architecture Lab:预备知识2【汇编call/leave/ret指令、CS:APP练习4.4】

chap4的练习4.4&#xff08;page.255&#xff09;让用Y86-64实现rsum&#xff08;递归求数组元素之和&#xff09;&#xff0c;提示为&#xff1a;先得到x86-64汇编代码&#xff0c;然后转换成Y86-64的 这是rsum的c实现&#xff1a; long rsum(long *start, long count) {if …

【HarmonyOS】掌握布局组件,提升应用体验

从今天开始&#xff0c;博主将开设一门新的专栏用来讲解市面上比较热门的技术 “鸿蒙开发”&#xff0c;对于刚接触这项技术的小伙伴在学习鸿蒙开发之前&#xff0c;有必要先了解一下鸿蒙&#xff0c;从你的角度来讲&#xff0c;你认为什么是鸿蒙呢&#xff1f;它出现的意义又是…

知识点 高精度运算

1.基础知识&#xff1a; int是一个机器字长&#xff0c;在32位机器上是32位&#xff0c;则表示范围是[-,-1]&#xff0c;可表示的最大整数不会超过。 long long是两个机器字长&#xff0c;表示范围是[-,-1]&#xff0c;可表示的最大整数不会超过。 2.适用于需要表示的数据过大…

C++实战:实现生命游戏

文章目录 一、实战概述二、实战步骤&#xff08;一&#xff09;编写生命头文件&#xff08;二&#xff09;创建生命实现文件&#xff08;三&#xff09;编写工具头文件&#xff08;四&#xff09;编写工具实现文件&#xff08;五&#xff09;编写主程序文件&#xff08;六&…

wordcloud,一个超强的 Python 库!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个超强的 Python 库 - wordcloud。 Github地址&#xff1a;https://github.com/amueller/word_cloud 词云图是一种独特而引人注目的数据可视化方式&#xff0c;通常用于显示…

第35集《佛法修学概要》

己四 、 精进度 分三&#xff1a;庚一、 精进自性。庚 二、趣入修习精进方便。 庚三、修习精进差别内容 请大家打开讲义第九十四页&#xff0c;我们看己四&#xff0c;精进度。 当我们从人天乘一个好人的阶段提升到一种菩萨道的修学&#xff0c;我们就要注意两个重点了。在我…

什么是内网穿透?

简介&#xff1a; 书面来说&#xff1a;内网穿透是一种网络技术&#xff0c;用于将内部私有网络中的服务暴露给公共网络&#xff0c;使得外部网络可以通过公网访问内网的服务。通常情况下&#xff0c;内网中的设备和服务由于位于私有网络中&#xff0c;无法直接通过公网IP进行…

9.1 Maven项目管理(❤❤❤❤)

9.1 Maven项目管理 1. Maven介绍2. 创建Maven项目2.1 创建2.2 结构分析3. Maven依赖管理3.1 简介3.2 设置下载镜像仓库4. 本地仓库与中央仓库5. Maven生命周期6. Maven插件技术6.1 build标签7. maven属性管理

Django从入门到精通(一)

目录 一、Django环境搭建与命令 1.1、安装 1.2、命令行 创建项目 编写代码 运行 app概念 1.3、Pycharm创建项目 1.4、虚拟环境 创建虚拟环境 - 命令行 介绍 操作 基本问题 Pycharm 项目虚拟环境 django虚拟环境【安装django最新版本】 django虚拟环境【安装指…

《软件方法》强化自测题-杂项(3)-少林足球巴别塔-不属于“软件方法建模师”考察范围

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 本套自测题不属于“软件方法建模师”考察范围。 自测链接&#xff1a;https://www.101test.com/cand/index?paperIdQR6CGK 1. [单选] 著名歌曲《橄榄树》&#xff08;不要问我从那…

端口映射的定义、特点、场景、实例、常见问题回答(Port Mapping)

目 录 一、端口映射&#xff08;Port Mapping&#xff09; 二、端口映射应用场景&#xff08;什么时候用到端口映射&#xff09; &#xff08;一&#xff09;、使用端口映射的条件 &#xff08;二&#xff09;使用端口映射的具体场景 三、端口映射技术的特点 …

内网安全管理系统(保密管理系统)

在当今信息化的时代&#xff0c;企业的内网已经成为其核心资产的重要组成部分。 随着企业的快速发展和信息化程度的提升&#xff0c;内网安全问题日益凸显&#xff0c;如何保障内网的安全和机密信息的保密性&#xff0c;已经成为企业亟待解决的问题。 内网安全管理系统(保密管…

Docker项目部署()

1.创建文件夹tools mkdir tools 配置阿里云 Docker Yum 源 : yum install - y yum - utils device - mapper - persistent - data lvm2 yum - config - manager -- add - repo http://mirrors.aliyun.com/docker- ce/linux/centos/docker - ce.repo 更新 yum 缓存 yum makec…

大路灯和护眼台灯哪个好?2024五款大路灯推荐

家用照明发展至今&#xff0c;从古时的匡衡凿壁借光读书&#xff0c;到后面的油灯、蜡烛等照明方式&#xff0c;再到近代的普通白炽灯&#xff0c;荧光灯、LED等电致发光灯具&#xff0c;发展到现在&#xff0c;科技在进步&#xff0c;文明在升级&#xff0c;照明灯具早已不再仅…

各种Linux版本安装Docker

文章目录 一、Ubuntu 20.04.61. 网卡和DNS配置2. Docker安装 二、CentOS Linux 7.91. 网卡和DNS配置2. Docker安装 三、Alibaba Cloud Linux 31. DNS配置2. repo说明3. Docker安装 四、验证是否安装成功 一、Ubuntu 20.04.6 1. 网卡和DNS配置 /etc/netplan 找到 *.yaml 文件 …

YOLOv8全网首发:新一代高效可形变卷积DCNv4如何做二次创新?高效结合SPPF

💡💡💡本文独家改进:DCNv4更快收敛、更高速度、更高性能,与YOLOv8 SPPF高效结合 收录 YOLOv8原创自研 https://blog.csdn.net/m0_63774211/category_12511737.html?spm=1001.2014.3001.5482 💡💡💡全网独家首发创新(原创),适合paper !!! 💡💡💡…

bgp选路

完成基础配置后&#xff1a; 配置bgp&#xff08;如r2&#xff09;&#xff1a; 全布配置与重发布完成后&#xff1a;全网可达 起源属性修改选路&#xff1a;

LeetCode 0082.删除排序链表中的重复元素 II:模拟

【LetMeFly】82.删除排序链表中的重复元素 II&#xff1a;模拟 力扣题目链接&#xff1a;https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/ 给定一个已排序的链表的头 head &#xff0c; 删除原始链表中所有重复数字的节点&#xff0c;只留下不同的数字…

2023年十款开源测试开发工具推荐(自动化、性能、造数据、流量复制)

1、AutoMeter-API 自动化测试平台 AutoMeter 是一款针对分布式服务&#xff0c;微服务 API 做功能和性能一体化的自动化测试平台&#xff0c;一站式提供发布单元&#xff0c;API&#xff0c;环境&#xff0c;用例&#xff0c;前置条件&#xff0c;场景&#xff0c;计划&#xf…