Linux高性能服务器-第六章-高级I/O

79 阅读7分钟

6.2 dup函数和dup2函数

#include <unistd.h>

// 创建一个新的文件描述符,与原描述符file_descriptor指向相同文件、管道或网络连接
// 返回系统当前可用的最小整数值作为新描述符
// 失败返回-1并设置errno
int dup( int file_descriptor );
// 返回第一个不小于file_descriptor_two整数值作为新描述符
int dup2(int file_descriptor_one, int file_descriptor_two );

/*通过dup/dup2创建的文件描述符不继承原文件描述符的属性,必须close-on-exec,non-blocking*/

示例:

...
...
int connfd = accept( ... );    			// 获得客户端socket连接
if(connfd < 0) {
    printf("errno : %d\n", errno);
} else {
    close(STDOUT_FILENO);				// 关闭标准输出文件描述符,其值为1
    dup(connfd);						// 复制connfd, 返回系统最小可用描述符,实际为1
    printf("abcd\n");					// 输出到标准输出的内容将被发送到客户端socket上
    close(connfd);						
}
...

6.3 readv和writev函数

分散读:readv将从数据从文件描述符读到分散内存块中;

集中写:writev将分散内存块中数据一并写到文件描述符中;

#include <sys/uio.h>

// count: vector数据长度,即有多少块内存区
// 成功返回读/写fd的字节数,失败返回-1并设置errno
ssize_t readv( int fd, const struct iovec* vector, int count );
ssize_t writev( int fd, const struct iovec* vector, int count );

考虑一个Web服务器。当其解析完一个HTTP请求,若目标文档存在且用户具有读取权限,那服务器需发送一个HTTP应答传输该文档。

该HTTP应答包含一个状态行、多个头部字段、一个空行和文档内容。其中,前三部分可能被Web服务器存放在一块内存中,文档内容则被存放在另一块单独内存中。此时,我们不需要将这两部分分开读再拼接到一起发送,而是直接使用writev函数集中写发送。

Web服务器上的集中写:

#define BUFFER_SIZE 1024
static const char* status_line[2] = { "200 OK", "500 Internal server error" };

int main( int argc, char* argv[] )
{
...
    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;
        // 缓存区header_buf已使用的字节空间长度
        int len = 0;
        if( stat( file_name, &file_stat ) < 0 )		//	目标文件不存在
        {
            valid = false;
        }
        else
        {
            if( S_ISDIR( file_stat.st_mode ) )		// 目标文件是个目录
            {
                valid = false;
            }
            else if( file_stat.st_mode & S_IROTH ) 	// 当前用户有读取目标文件的权限
            {
                int fd = open( file_name, O_RDONLY );
                // 动态分别缓冲区大小为目标文件大小加1
                file_buf = new char [ file_stat.st_size + 1 ];
                memset( file_buf, '\0', file_stat.st_size + 1 );
                // 读入目标文件到缓冲区中
                if ( read( fd, file_buf, file_stat.st_size ) < 0 )		
                {
                    valid = false;
                }
            }
            else
            {
                valid = false;
            }
        }
        // 如果目标文件有效,则发送HTTP应答
        if( valid )
        {
            // HTTP状态行加入缓冲区
            ret = snprintf( header_buf, BUFFER_SIZE-1, "%s %s\r\n", "HTTP/1.1", status_line[0] );
            len += ret;
            // Content-Length头部字段加入缓冲区
            ret = snprintf( header_buf + len, BUFFER_SIZE-1-len, 
                             "Content-Length: %d\r\n", file_stat.st_size );
            len += ret;
            // 空行加入缓冲区
            ret = snprintf( header_buf + len, BUFFER_SIZE-1-len, "%s", "\r\n" );
            // 利用writev将header_buf和file_buf的内容一并写出
            struct iovec iv[2];
            iv[ 0 ].iov_base = header_buf;
            iv[ 0 ].iov_len = strlen( header_buf );
            iv[ 1 ].iov_base = file_buf;
            iv[ 1 ].iov_len = file_stat.st_size;
            ret = writev( connfd, iv, 2 );
        }
        else			// 若目标文件无效,发送错误码"500 Internal server error"
        {
            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;
}

6.4 sendfile函数

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

#include <sys/sendfile.h>

// in_fd: 待读出数据的文件描述符, 必须指向真实文件,不能是socket和管道
// out_fd: 待写入数据的文件描述符, 必须是一个socket
// offset: 指定从读入文件流哪个位置开始读, 若为空,则默认起始位置
// count: 传输字节数
// 成功返回传输字节数,失败返回-1并设置errno
ssize_t sendfile( int out_fd, int in_fd, off_t* offset, size_t count );

由上可见,sendfile是为在网络中传输文件而设计的。

...
int filefd = open( file_name, O_RDONLY );
assert( filefd > 0 );
struct stat stat_buf;
fstat( filefd, $stat_buf );
...
sendfile( connfd, filefd, NULL, stat_buf.st_size );

6.5 mmap和munmap函数

mmap函数用于申请一段内存空间,可将这段空间作为进程间通信的共享内存,也可将文件直接映射到其中。

munmap则释放由mmap创建的内存空间。

#include <sys/mman.h>
/* 
	start: 申请内存的起始地址,若设置为NULL,则系统自动分配
	length: 内存段长度
	prot: 内存段访问权限,可由以下值按位或:
		PROT_READ 可读 PROT_WRITE 可写 PROT_EXEC 可执行 PROT_NONE 不可访问
	flags: 控制内存段内容被修改后程序的行为,取值见下表,各值可按位或
	fd: 被映射文件的描述符
	offset: 从文件何处开始映射
*/
// 成功返回指向目标内存区域的指针,失败返回MAP_FILLED并设置errno
void* mmap( void* start, size_t length, int prot, int flags, int fd, off_t offset );
// 成功时返回0, 失败返回-1并设置errno
int munmap( void* start, size_t length );

mmap的flags参数常用值及含义:

mmap_flags.png

6.6 splice函数

splic函数用于在两个文件描述之间移动数据,同样是零拷贝操作。

#include <fcntl.h>
/*
	fd_in: 待输入数据的文件描述符
	off_in: 若fd_in是一个管道描述符,off_in必须设为NULL。
			若值为NULL,表示从当前偏移位置开始读;
	len: 移动数据的长度
	flags: 控制数据如何移动,取值见下表
	fd_in和fd_out必须有一个是管道描述符
	成功时返回移动数据字节量,失败返回-1并设置errno
*/
ssize_t splice( int fd_in, loff_t* off_in, in fl_out, loff_t* off_out, size_t len, unsigned int flags );

splice的flags常用值及含义:

splice_flags.png

零拷贝回射服务器,将客户端发送的数据原样返回给客户端:

int main( int argc, char* argv[] )
{
 	...
    ...
    int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
    if ( connfd < 0 )
    {
        printf( "errno is: %d\n", errno );
    }
    else
    {
        int pipefd[2];
        assert( ret != -1 );
        // 创建管道
        ret = pipe( pipefd );			
        // 将connfd上流入的客户端数据定向到管道中
        ret = splice( connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); 
        assert( ret != -1 );
        // 将管道数输出定向到connfd端口中
        ret = splice( pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
        assert( ret != -1 );
        close( connfd );
    }

    close( sock );
    return 0;
}

6.7 tee函数

tee函数在两个管道描述符之间复制数据,也是零拷贝操作。tee操作不消耗数据,因此源文件描述符中的数据仍可使用。

#include <fcntl.h>
/*
	fd_in和fd_out都必须是管道文件描述符
	成功返回移动数据字节数,失败返回-1并设置errno
*/
ssize_t tee( int fd_in, int fd_out, size_t len, unsigned int flags );

6.8 fcntl函数

file control,提供了对文件描述符的各种操作。

#include <fcntl.h>
// cmd指定执行何种操作,根据操作要求,可能还需设置第三个可选参数,见下表
// 成功返回值见下表,失败返回-1并设置errno
int fcntl( int fd, int cmd, ...);

fcntl支持的常用参数及参数:

fcntl_cmd1.png

fcntl_cmd2.png

fcntl将一个文件描述符设置为非阻塞:

int setnonblocking( int fd ) 
{
    int old_opt = fcntl( fd, F_GETFL);		// 获取文件描述符旧状态
    int new_opt = old_opt | O_NONBLOCK;		// 设置非阻塞
    fcntl( fd, F_SETFL, new_opt );			
    return old_opt;							// 返回旧状态,以便日后恢复
}

SIGIO和SIGURG信号必须与某个文件描述符关联才可使用:

当被关联文件描述符可读或可写时,将触发SIGIO信号;当被关联文件描述符(必须为socket)上有带外数据时,将触发SIGURG信号。

将信号与文件描述符关联的方法,就是使用fcntl函数为目标文件描述符指定宿主进程或进程组,那么被指定的宿主进程或进程组将捕获这两个信号,使用SIGIO时,还需利用fcntl设置其O_ASYNC异步标志。