Linux高级I/O函数---Linux高性能服务器编程学习笔记

管道pip函数

pipe函数可用于创建一个管道,以实现进程间通信。它表现出来的形式将前面每一个进程的输出(stdout)直接作为下一个进程的输入(stdin)。

#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]则只能用于往管道写入数据,而不能反过来使用。默认情况下,这一对文件描述符都是阻塞的。

管道内部传输的数据是字节流,这和TCP字节流的概念相同。

用层程序能往一个TCP连接中写入多少字节的数据,取决于对方的接收通告窗口的大小和本端的拥塞窗口的大小。而管道本身拥有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少字节的数据。自Linux 2.6.11内核起,管道容量的大小默认是65536字节。我们可以使用fcntl函数来修改管道容量(见后文)。

一个管道进程通信实例

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
 
#define MAXLINE (2014)
 
int main(void)
{
	int n,fd[2];//保存管道返回的两个文件描述符
	pid_t pid;
	char line[MAXLINE];
	if(pipe(fd)<0)//创建管道,fd[0]是读端,fd[1]是写端。
		printf("pipe error");
	if((pid=fork())<0)//创建进程
		printf("fock error");//创建进程失败
	else if(pid>0)//pid大于零,为父进程,pid的值是子进程的
	{
		close(fd[0]);//关闭读端
		printf("#the parent process pid %d\n",getpid());//返回当前进程的id
		printf("#the children pid is %d\n",pid);
		printf("#the process write to pipe: hello world\n");
		write(fd[1],"hello world\n",12);//向写端写入12个字节数据
	}
	else
	{
		close(fd[1]);//关闭写端
		printf("$the children process pid %d\n",getpid());
		printf("$the parent process pid %d\n",getppid());
		n = read(fd[0],line,MAXLINE);
		//write(STDOUT_FILENO,line,n);//把数据写入标准输出文件描述符
        printf("%s",line);
	}
	exit(0);
}

result:
result

双向管道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。

实例:

#include <stdio.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <error.h> 
#include <errno.h> 
#include <sys/socket.h> 
#include <stdlib.h>

#define MAXLINE (2014)
 
int main(void)
{
	int n,fd[2];//保存管道返回的两个文件描述符
	pid_t pid;
	char line[MAXLINE];
	if(socketpair(AF_UNIX,SOCK_STREAM, 0,fd)<0)//创建管道,fd[0]fd[1]均可读可写。
		printf("socketpair error");
	if((pid=fork())<0)//创建进程
		printf("fock error");//创建进程失败
	else if(pid>0)//pid大于零,为父进程,pid的值是子进程的
	{
		close(fd[0]);//关闭fd[0]端
		printf("#the parent process pid %d\n",getpid());//返回当前进程的id
		printf("#the children pid is %d\n",pid);
		printf("#the process write to pipe: hello world\n");
		write(fd[1],"father:hello world\n",19);//向写端写入19个字节数据

        n = read(fd[1],line,MAXLINE);
		//write(STDOUT_FILENO,line,n);//把数据写入标准输出文件描述符
        printf("father recv:%s",line);
	}
	else
	{
		close(fd[1]);//关闭fd[1]端
		printf("$the children process pid %d\n",getpid());
		printf("$the parent process pid %d\n",getppid());
		n = read(fd[0],line,MAXLINE);
		//write(STDOUT_FILENO,line,n);//把数据写入标准输出文件描述符
        printf("son recv:%s",line);

        write(fd[0],"son:hello world\n",16);//向写端写入16个字节数据
	}
	exit(0);
}

重定向函数dup/dup2

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

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

#include <stdio.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <error.h> 
#include <errno.h> 
#include <sys/socket.h> 
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
    int fd;
    int new_fd;

    fd = open("./test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    printf("fd = %d\n", fd);

    new_fd = dup(fd);    //复制文件描述符,把fd复制给new_fd
    printf("new_fd = %d\n", new_fd);
    write(new_fd, "hello\n", 6);

    close(fd);
    close(new_fd);

    return 0;
}

dup2可用作输入输出重定向,典型的是系统的stdin和stdout重定向。


int main(void)
{
    int fd;
    int new_fd;

    fd = open("./test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    printf("fd = %d\n", fd);

    //dup2(fd, STDOUT_FILENO);
    dup2(fd, 1);
    printf("hello world!!\n");

    return 0;
}

分散读和集中写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结构数组,该结构体描述一块内存区。count参数是vector数组的长度,即有多少块内存数据需要从fd读出或写到fd。readv和writev在成功时返回读出/写入fd的字节数,失败则返回-1并设置errno。它们相当于简化版的recvmsg和sendmsg函数。
例子 web服务器的集中写:

#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
/*定义两种HTTP状态码和状态信息*/
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;
        /*缓存区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 )/*当前用户有读取目标文件的权限*/
            {
                /*动态分配缓存区file_buf,并指定其大小为目标文件的大小file_stat.st_size加1,然后将目标文件读入缓存区file_buf中*/
                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;
                }
            }
            else
            {
                valid = false;
            }
        }
        /*如果目标文件有效,则发送正常的HTTP应答*/
        if( valid )
        {
            /*下面这部分内容将HTTP应答的状态行、“Content-Length”头部字段和一个空行依次加入header_buf中*/
            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" );
            /*利用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/*如果目标文件无效,则通知客户端服务器发生了“内部错误”*/
        {
            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;
}


零拷贝sendfile函数

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

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

in_fd参数是待读出内容的文件描述符,out_fd参数是待写入内容的文件描述符。offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置。count参数指定在文件描述符in_fd和out_fd之间传输的字节数。sendfile成功时返回传输的字节数,失败则返回-1并设置errno。in_fd必须指向真实的文件,不能是socket和管道;而out_fd则必须是一个socket。sendfile几乎是专门为在网络上传输文件而设计的。

#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 );

    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( connfd, filefd, NULL, stat_buf.st_size );
        close( connfd );
    }

    close( sock );
    return 0;
}


零拷贝splice函数

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

#include<fcntl.h>
ssize_t splice(int fd_in,loff_t*off_in,intfd_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参数则控制数据如何移动。
flag

使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。splice函数调用成功时返回移动字节的数量。它可能返回0,表示没有数据需要移动,这发生在从管道中读取数据(fd_in是管道文件描述符)而该管道没有被写入任何数据时。splice函数失败时返回-1并设置errno。
使用splice函数来实现一个零拷贝的回射服务器,它将客户端发送的数据原样返回给客户端

#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];
        assert( ret != -1 );
        ret = pipe( pipefd );
        ret = splice( connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); 
        assert( ret != -1 );
        ret = splice( pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
        assert( ret != -1 );
        close( connfd );
    }

    close( sock );
    return 0;
}


零拷贝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。
利用tee函数和splice函数,同时输出数据到终端和文件的程序

#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 );

	//close( STDIN_FILENO );
	// dup2( pipefd_stdout[1], STDIN_FILENO );
	//write( pipefd_stdout[1], "abc\n", 4 );
	/*将标准输入内容输入管道pipefd_stdout*/
	ret = splice( STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
	assert( ret != -1 );
	/*将管道pipefd_stdout的输出复制到管道pipefd_file的输入端*/
	ret = tee( pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK ); 
	assert( ret != -1 );
	/*将管道pipefd_file的输出定向到文件描述符filefd上,从而将标准输入的内容写入文件*/
	ret = splice( pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
	assert( ret != -1 );
	/*将管道pipefd_stdout的输出定向到标准输出,其内容和写入文件的内容完全一致*/
	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;
}

共享内存mmap函数和munmap函数

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

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

代码后面补。

文件描述符控制fcntl函数

fcntl函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制操作。

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

fcntl
将文件描述符设置为非阻塞的

int setnonblocking(int fd){
	int old_option=fcntl(fd,F_GETFL);/*获取文件描述符旧的状态标志*/
	int new_option=old_option|O_NONBLOCK;/*设置非阻塞标志*/
	fcntl(fd,F_SETFL,new_option);
	return old_option;/*返回文件描述符旧的状态标志,以便日后恢复该状态标志*/
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值