常用Linux网络编程相关的高级I/O函数:
- 用于创建文件描述符的函数,包括pipe、dup/dup2函数
- 用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee函数
- 用于控制I/O行为和属性的函数,包括fctnl函数
pipe函数
pipe函数:创建一个管道,以实现进程间的通信。
1 2 3 4 5 6 7 8 9 10 11
| #include<unistd.h> int pipe(int fd[2]);
|
默认情况下,一对文件描述符都是阻塞的。如果用read系统调用来读取一个空的管道,则read将被阻塞,直到管道内有数据可读;如果用write系统调用来往一个满的管道中写入数据,则write亦将被阻塞,直到管道有足够多的空闲空间可用。
如果应用程序将fd[0]和fd[1]都设置为非阻塞的,则read和write会有不同的行为。
如果写端fd[1]的引用计数减少到0,表面没有任何进程需要向管道内写入数据,则该管道的读端fd[0]和read操作将返回0,即读取到了文件结束标记EOF。
如果读端fd[0]的引用计数减少到0,表面没有任何进程需要从管道内读取数据,则该管道的写端fd[1]和write操作将失败,并引发SIGPIPE信号。
管道内部传输的数据是字节流。管道容量的大小默认是65536字节。可以使用fcntl函数来修改管道容量。
socket的基础API中有一个socketpair函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include<sys/types.h> #include<sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2]);
|
dup和dup2函数
把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接。通过用于复制文件描述符的dup或dup2函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include<unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
|
注意:通过dup和dup2创建的文件描述符并不继承原文件描述符的属性,比如close-on-exec和non-blocking等。
利用dup函数实现了一个基本的CGI服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| #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 ); dup( connfd ); printf( "abcd\n" ); close( connfd ); }
close( sock ); return 0; }
|
readv和writev函数
readv函数:将数据从文件描述符读到分散的内存块中,分散读。
writev函数:将多块分散的内存数据一并写入文件描述符中,集中写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #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);
|
Web服务器上的集中写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
| #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> #include <sys/uio.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 { 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 ) ) { valid = false; } else if( file_stat.st_mode & S_IROTH ) { 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; } } if( valid ) { 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; 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函数:在两个文件描述符之间直接传递数据(完全在内核中操作),避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。
1 2 3 4 5 6 7 8 9 10 11 12
| #include<sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
|
注:in_fd必须是支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道;out_fd必须是一个socket。
利用sendfile函数将服务器上的一个文件传送给客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| #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; }
|
将目标文件作为第3个参数传递给服务器程序,客户telnet到该服务器上即可获得该文件。
与6-2estwritev.cpp相比,6-3testsendfile.cpp没有为目标文件分布任何用户空间的缓存,也没有执行读取文件的操作,同样实现了文件的发送,显然效率更高。
mmap和munmap函数
mmap函数:用于申请一段内存空间。可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。
munmap函数:释放由mmap创建的这段内存空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #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);
|
splice函数
splice函数:用于在两个文件描述符之间移动数据,也是零拷贝操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #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);
|
使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。
使用splice函数来实现一个零拷贝的回射服务器,它将客户端发送的数据原样返回给客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| #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; }
|
通过splice函数将客户端的内容读入到pipefd[1]中,然后再使用splice函数从pipefd[0]中读出该内容到客户端,从而实现了简单高效的回射服务。整个过程未执行recv/send操作,因此也未涉及用户空间和内核空间之间的数据拷贝。
tee函数
tee函数:在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include<fcntl.h> ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
|
利用tee函数和splice函数,实现Linux下tee程序的基本功能(同时输出数据到终端和文件的程序,不要和tee函数混淆)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| #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 );
ret = splice( STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); assert( ret != -1 );
ret = tee( pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK ); assert( ret != -1 );
ret = splice( pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); assert( ret != -1 );
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; }
|
fcntl函数
fcntl函数:提供了对文件描述符的各种控制操作。对于控制文件描述符常用的属性和行为,fcntl函数是由POSIX规范指定的首选方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include<fcntl.h> int fcntl(int fd, int cmd,…);
|
在网络编程中,fcntl函数通常用来将一个文件描述符设置为非阻塞的。
将文件描述符设置成非阻塞的:
1 2 3 4 5 6
| 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; }
|
SIGIO和SIGURG这两个信号与其他Linux信号不同,它们必须与某个文件描述符相关联方可使用:当被关联的文件描述符可读或可写时,系统将触发SIGIO信号;当被关联的文件描述符(而且必须是一个socket)上有带外数据可读时,系统将触发SIGURG信号。
使用SIGIO时,还需要利用fcntl设置其O_ASYNC标志(异步I/O标志,不过SIGIO信号模型并非真正意义上的异步I/O模型)。