《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");// 关闭 connfdclose(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 );// 创建监听socketint 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 );}// 关闭监听socketclose( 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.mzph.cn/news/636114.shtml

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

相关文章

TypeScript语法总结

JavaScript 与 TypeScript 的区别 TypeScript 是 JavaScript 的超集&#xff0c;扩展了 JavaScript 的语法&#xff0c;因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改&#xff0c;TypeScript 通过类型注解提供编译时的静态类型检查。 TypeScript 可处理已…

从数据角度分析年龄与NBA球员赛场表现的关系【数据分析项目分享】

好久不见朋友们&#xff0c;今天给大家分享一个我自己很感兴趣的话题分析——NBA球员表现跟年龄关系到底大不大&#xff1f;数据来源于Kaggle&#xff0c;感兴趣的朋友可以点赞评论留言&#xff0c;我会将数据同代码一起发送给你。 目录 NBA球员表现的探索性数据分析导入Python…

【 Qt 快速上手】-①- Qt 背景介绍与发展前景

文章目录 1.1 什么是 Qt1.2 Qt 的发展史1.3 Qt 支持的平台1.4 Qt 版本1.5 Qt 的优点1.6 Qt的应用场景1.7 Qt的成功案例1.8 Qt的发展前景及就业分析行业发展方向就业方面的发展前景 1.1 什么是 Qt Qt 是一个跨平台的 C 图形用户界面应用程序框架。它为应用程序开发者提供了建立…

DBA技术栈MongoDB:简介

1.1 什么是MongoDB&#xff1f; MongoDB是一个可扩展、开源、表结构自由、用C语言编写且面向文档的数据库&#xff0c;旨在为Web应用程序提供高性能、高可用性且易扩展的数据存储解决方案。 MongoDB是一个介于关系数据库和非关系数据库之间的产品&#xff0c;是非关系数据库当…

linux下USB抓包和分析流程

linux下USB抓包和分析流程 在windows下抓取usb包时可以通过wireshark安装时安装USBpcap来实现usb抓包&#xff0c;linux下如何操作呢&#xff1f; 是基于usbmon&#xff0c;本博客简单描述基于usbmon在linux系统上对通过usb口进行发送和接收的数据的抓包流程&#xff0c;分别描…

SCI期刊查询利器:影响因子和分区情况一站式查询

参考 本文仅作为学术分享,如果侵权,会删文处理 期刊的影响因子,最传统也最靠谱的方法就是去 Journal Citation Reports 官方平台上面查询,JCR 平台直接输入期刊名称检索,或者按照类别查找期刊:如果在校外没有访问JCR的权限,可以购买80图书馆的WOS套餐,仅需38元,不到一…

【51单片机系列】proteus仿真单片机的串口通信

本文参考&#xff1a;https://zhuanlan.zhihu.com/p/425809292。 在proteus之外使用串口软件和单片机通信。通过在proteus设计一个单片机接收PC发送的数据&#xff0c;并将接收的数据发送出去&#xff0c;利用软件【Configure Virtual Serial Port Driver】创建一对虚拟串口&am…

Linux指令权限知识点总结

目录 周边知识 基础指令思维导图 权限思维导图 周边知识 大多数后端操作系统都是Linux操作系统操作系统是管理软件和硬件的软件Linux是一款操作系统Linux分为技术版本和商业版本Linux的文件是以多叉树的形式构建隐藏文件 . 和 ...可以表示当前路径。可以形成可执行文件&a…

关于ElasticSearch,你应该知道的

一、集群规划优化实践 1、基于目标数据量规划集群 在业务初期&#xff0c;经常被问到的问题&#xff0c;要几个节点的集群&#xff0c;内存、CPU要多大&#xff0c;要不要SSD&#xff1f; 最主要的考虑点是&#xff1a;你的目标存储数据量是多大&#xff1f;可以针对目标数据…

用LED数码显示器循环显示数字0~9

#include<reg51.h> // 包含51单片机寄存器定义的头文件 /************************************************** 函数功能&#xff1a;延时函数&#xff0c;延时一段时间 ***************************************************/ void delay(void) { unsigned …

phpmyadmin 创建服务器

phpmyadmin默认的服务器是localhost 访问setup&#xff0c;创建新的服务器 添加服务器信息 点击应用&#xff0c;服务器创建成功 下载配置文件config.inc.php&#xff0c;放到WWW目录下 可再次访问setup&#xff0c;发现已配置过了 访问登录页面&#xff0c;发现可选…

第十一章 请求响应

第十一章 请求响应 1.概述2.请求-postman工具3.请求-简单参数&实体参数4.请求-数组集合参数5.请求-日期参数&JSON参数6.请求-路径参数7.响应-ResponseBody&统一响应结果8.响应-案例 1.概述 将前端发送的请求封装为HttpServletRequest对象 在通过HttpServletRespo…

OpenCV-Python(51):基于Haar特征分类器的面部检测

目标 学习了解Haar 特征分类器为基础的面部检测技术将面部检测扩展到眼部检测等。 基础 以Haar 特征分类器为基础的对象检测技术是一种非常有效的对象检测技术(2001 年Paul_Viola 和Michael_Jones 提出)。它是基于机器学习的,通过使用大量的正负样本图像训练得到一个cascade_…

【Linux取经路】初探进程地址空间

文章目录 一、历史问题回顾二、语言层面的地址空间2.1 验证 三、虚拟地址的引入3.1 初步解释这种现象——引入地址空间的概念3.2 再来粗粒度理解上面的现象 四、细节解释4.1 地址空间究竟是什么&#xff1f;4.2为什么要有地址空间4.3 页表4.3.1 CR3寄存器4.3.2 页表是由页表项组…

【Linux的基本指令】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 1、ls 指令 2、 pwd命令 3、cd 指令 4、touch指令 5、mkdir指令&#xff08;重要&#xff09; 6、rmdir指令 && rm 指令&#xff08;重要&#xff09;…

前后端分离,仓储模式的医院安全(不良)事件报告系统

医院安全&#xff08;不良&#xff09;事件报告系统源码&#xff0c;PHP语言开发 医院不良事件上报系统&#xff0c;按照不良事件的管理部门不同&#xff0c;分为护理不良事件、药品不良反应事件、医技不良事件、院内感染事件、输血不良反应事件、器械不良事件、信息不良事件、…

国产AI新篇章:书生·浦语2.0带来200K超长上下文解决方案

总览&#xff1a;大模型技术的快速演进 自2023年7月6日“书生浦语”&#xff08;InternLM&#xff09;在世界人工智能大会上正式开源以来&#xff0c;其在社区和业界的影响力日益扩大。在过去半年中&#xff0c;大模型技术体系经历了快速的演进&#xff0c;特别是100K级别的长…

力扣:474. 一和零(动态规划)(01背包)

题目&#xff1a; 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。 请你找出并返回 strs 的最大子集的长度&#xff0c;该子集中 最多 有 m 个 0 和 n 个 1 。 如果 x 的所有元素也是 y 的元素&#xff0c;集合 x 是集合 y 的 子集 。 示例 1&#xff1a; 输入&#…

JOSEF约瑟 零序过流继电器LGL-110/AC AC220V 0.01~9.99A 柜内安装

LGY 、LGL零序过电压继电器 系列型号 LGY-110零序过电压继电器&#xff1b; LGL-110零序过电压继电器&#xff1b; LGL-110/AC零序过电压继电器&#xff1b; LGL-110静态零序过电流继电器 &#xff11; 应用 LGL-110 型零序过电流继电器用作线路和电力设备的零序过电流保护。…

一文详解Bitcoin Wallet(btc钱包),推荐bitget钱包

​ 比特币&#xff08;BTC&#xff09;是什么&#xff1f; 比特币&#xff08;BTC&#xff09;于 2008 年由中本聪创建&#xff0c;是一个去中心化的点对点网络。这个开创性的系统运用了密码学技术和分布式账本技术&#xff0c;无需中央权威机构的验证。比特币的诞生标志着去中…