网站租用价格,上海网站排名seo公司哪家好,多商户商城服务态度好,百顺网站建设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.…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 FileEOF反之如果管道的读端文件描述 符fd[0]的引用计数减少至0即没有任何进程需要从管道读取数据则针对该管道的写端文 件描述符fd[1]的write操作将失败并引发SIGPIPE信号。关于SIGPIPE信号我们将在 第10章讨论。
pipe 函数是用于创建管道的系统调用。管道是用于进程间通信的一种机制它可以在两个进程之间传递数据。pipe 函数的声明如下
#include unistd.hint pipe(int fd[2]);参数 fd: 用于存储管道两端文件描述符的数组。fd[0] 是用于读取的文件描述符fd[1] 是用于写入的文件描述符。 返回值 如果成功返回 0如果失败返回 -1并设置 errno。
使用示例
#include stdio.h
#include unistd.hint 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 函数。它能够方便地创建双向管道。其定义如下
#includesys/types.h
#includesys/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.hint 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服务器的基本工作原理。下面是对每个步骤的解释 关闭标准输出文件描述符 (STDOUT_FILENO) 通过调用close(STDOUT_FILENO)关闭标准输出文件描述符。这是因为在CGI服务器的工作模式中我们希望将动态生成的内容发送到与客户端连接相关联的套接字而不是输出到服务器程序的终端。 复制socket文件描述符 (connfd) 使用dup(connfd)将套接字文件描述符 connfd 复制到系统中最小的可用文件描述符而这个最小的可用文件描述符实际上就是关闭的标准输出文件描述符 STDOUT_FILENO 的值。这意味着现在套接字文件描述符 connfd 成为了标准输出文件描述符的副本。 输出到标准输出 使用 printf 输出内容在这里是 “abcd”。由于标准输出文件描述符已经被复制为与客户端连接相关的套接字所以 printf 的输出实际上会被发送到客户端而不是显示在服务器程序的终端上。 客户端接收 因为标准输出已被重定向到与客户端连接的套接字所以客户端将接收到服务器发送的 “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.hint 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.hint 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.hint 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.hint 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;
}注释和解释 创建套接字、绑定、监听通过 socket、bind、listen 创建并设置服务器套接字。 接受客户端连接使用 accept 函数等待客户端连接得到连接套接字 connfd。 创建管道使用 pipe 创建一个管道pipefd[0] 是读取端pipefd[1] 是写入端。 通过 splice 实现数据传输使用两次 splice 函数第一次从连接套接字 connfd 中读取数据并写入管道第二次从管道读取数据并写入连接套接字。这样实现了零拷贝避免了数据在用户空间和内核空间之间的复制。 关闭连接套接字关闭已经处理完的连接套接字。 关闭监听套接字关闭服务器监听套接字。
我们通过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.hint 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;
}作用 该程序通过 splice 和 tee 函数实现了将标准输入的内容同时写入文件和标准输出的功能。使用管道和文件描述符传输数据无需用户空间和内核空间之间的数据拷贝从而提高了效率。
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.hint fcntl( int fd, int cmd, ... );解释
fd 文件描述符是需要进行操作的文件或套接字的标识符。cmd 控制命令指定对文件描述符 fd 进行的操作。
fcntl 函数用于对文件描述符进行各种控制操作取决于 cmd 参数的值。该函数的第三个参数 arg 的具体含义取决于 cmd 的值。
常见的 cmd 可选值
F_DUPFD 复制文件描述符。arg 为新的文件描述符的最小允许值。F_GETFL 获取文件状态标志。arg 为无符号整数表示文件的状态标志。F_SETFL 设置文件状态标志。arg 为要设置的状态标志的位掩码。F_GETLK 获取文件锁信息。arg 为指向 struct flock 结构的指针用于存储锁信息。F_SETLK 设置文件锁。arg 为指向 struct flock 结构的指针用于设置锁信息。F_SETLKW 设置文件锁如果无法获取锁则阻塞。arg 为指向 struct flock 结构的指针。
示例
#include fcntl.h
#include unistd.h
#include stdio.hint 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函数。