从零到负一

Socket编程基础6 - 高级IO函数

2020/06/12

这篇文章总结《Linux高性能服务器编程》第6章 - 高级I/O函数介绍的几个函数。之前在Linux下编程少,因此大部分函数都不清楚是干什么的,通过这章正好学习这几个函数,了解如何使用。

pipe

pipe()函数主要用于进程间通信,其函数原型如下:

1
int pipe(int pipefd[2]);

该函数的参数是一对fd,它们是单向的,其中fd[0]只能读,fd[1]只能写,默认情况下它们都是阻塞的。和socket有关的管道函数是socketpair(),它的函数原型是:

1
int socketpair(int domain, int type, int protocol, int sv[2]);

该函数的前3个参数和socket的参数一样,只是在Linux下domain只能是AF_UNIX(只能在本地使用)或者AF_TIPC;最后一个参数也是一对fd,它们是双向的,既可以读也可以写。

dup/dup2

dup()函数可以将指定的fd复制给新的fd,这个函数返回值就是新的fd,并且该fd是当前系统可用的最小fddup2()dup()函数类似,只是我们可以指定复制fd的编号,这两个函数原型如下:

1
2
3
// 发生错误,返回-1;否则返回新的fd
int dup(int oldfd);
int dup2(int oldfd, int newfd);

dup2()函数处理工作时:

  1. 如果newfd是已经被打开的,那么它会被原子的复制;

  2. 如果oldfd无效,那么newfd是不会被关掉的;

  3. 如果oldfdnewfd相同,那么该函数什么都不做,最后返回newfd

我们可以用这个函数将STDOUT_FILENO复制到用户定义的的fd中,那么之后输出到STDOUT_FILENO的函数都将输出到该fd

readv/writev

这两个函数可以读取/写入不连续的内存空间,它们最大的特点是读/写操作都是原子的。如果使用普通read()/write()函数,每个buffer都需要进行一次读/写操作,因此容易和其他进程产生竞争。这两函数原型如下:

1
2
size_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

其中struct iovec的结构如下:

1
2
3
4
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};

我们可以设置读写iovcnt个该结构体,每个结构体指定了buffer的地址和大小。现在的Linux支持最大1024个buffer,这个值是由IOV_MAX决定的。Linux: When to use scatter/gather IO (readv, writev) vs a large buffer with fread对该函数的用处做了基本的说明。

sendfile

sendfile()是一个零拷贝函数,也就是不需要数据在用户buffer和内核buffer之间进行相互拷贝,因此效率很高。该函数的原型如下:

1
2
// 返回拷贝成功的字节数或者-1
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

其中out_fd可以是任意的文件描述符(Linux 2.6.33后),而in_fd必须是支持mmap的文件描述符(不能是socket)。offset是指向存储in_fd偏移量变量的指针,count是想复制的字节数。我们可以很容易的用这个函数将读取的文件描述内容直接输出到socket。

splice

splice()函数也是一个零拷贝函数,但它必须在管道和其他文件描述符中进行数据的传输。其函数原型如下:

1
2
3
// 返回拷贝成功的字节数或者-1
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是管道,那么off_in必须NULLfd_out也同样如此;len是需要拷贝的字节数,flags用于控制数据如何移动。下面这个例子是用splice()实现简单的回射服务:

1
2
3
4
5
6
7
8
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 );

connfd接收到数据后将数据拷贝到pipefd[1],然后数据通过pipefd[1]流到pipefd[0],最后再写入connfdSPLICE_F_MORE告诉内核之后调用splice()会读取更多的数据。

tee

tee()函数和splice()函数类似,都是零拷贝函数。不过tee()用于复制管道,复制后原管道还留有数据(和splice()sendfile()不同)。该函数的原型如下:

1
2
// 返回复制数据的字节数(0表示没有复制)或者-1
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

fd_infd_out必须是管道,其他文件描述符不支持。

fcntl

fcntl()函数实现对文件描述符的控制,可以修改文件描述符的各种属性和行为。函数原型如下:

1
2
// 根据不同的cmd会有不同的返回,-1表示错误
int fcntl(int fd, int cmd, ... /* arg */ );

这个函数支持的命令很多,这里给一个简单的例子,其他命令可以用man查询。这个例子修改文件描述符为非阻塞:

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;
}

mmap/munmap

mmap()用于申请一块内存空间用于进程间的通信,同时我们也可以把文件映射到这块空间;munmap()用于释放这块空间。这里我只是简单介绍下这两个函数,之后会在多进程的总结中详细说明这两个函数。

1
2
3
4
5
// 成功的话,返回该空间的地址;失败返回(void *)-1
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
// 成功返回0,失败返回-1
int munmap(void *addr, size_t length);

addr是分配内存的起始地址,如果是NULL那么就内核分配一个地址;length是需要空间的大小;prot是设置读写权限;flags用于设置分配的内存被使用后程序的行为。fd是需要映射的文件描述符;offset是文件映射地址的偏移量。

CATALOG
  1. 1. pipe
  2. 2. dup/dup2
  3. 3. readv/writev
  4. 4. sendfile
  5. 5. splice
  6. 6. tee
  7. 7. fcntl
  8. 8. mmap/munmap