Linux 网络编程学习笔记——六、高级 I/O 函数

目录

一、pipe 函数

pipe 函数可用于创建一个管道,以实现进程间通信,定义如下:

#include 
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 会有不同的行为。如果管道的写端文件描述符 fd[1] 的引用计数减少至 0 ,即没有任何进程需要往管道中写入数据,则针对该管道的读端文件描述符 fd[0] 的 read 操作将返回 0 ,即读取到了文件结束标记(End Of File,EOF);反之,如果管道的读端文件描述符 fd[0] 的引用计数减少至 0 ,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符 fd[1] 的 write 操作将失败,并引发 SIGPIPE 信号。

管道内部传输的数据是字节流,这和 TCP 字节流的概念相同。但二者又有细微的区别。应用层程序能往一个 TCP 连接中写入多少字节的数据,取决于对方的接收通告窗口的大小和本端的拥塞窗口的大小。而管道本身拥有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少字节的数据。自 Linux 2.6.11 内核起,管道容量的大小默认是 65536 字节。我们可以使用 fcntl 函数来修改管道容量。 此外 socket 的基础 API 中有一个 socketpair 函数。它能够方便地创 建双向管道。其定义如下:

#include
#include
int socketpair(int domain,int type,int protocol,int fd[2]);

socketpair 前三个参数的含义与 socket 系统调用的三个参数完全相同,但 domain 只能使用 UNIX 本地域协议族 AF_UNIX ,因为我们仅能在本地使用这个双向管道。最后一个参数则和 pipe 系统调用的参数一 样,只不过 socketpair 创建的这对文件描述符都是既可读又可写的。socketpair 成功时返回 0 ,失败时返回 -1 并设置 errno 。

二、dup 函数和 dup2 函数

有时我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接(比如 CGI 编程)。这可以通过下面的用于复制文件描述符的 dup 或 dup2 函数来实现:

#include
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 。

注意,通过 dup 和 dup2 创建的文件描述符并不继承原文件描述符的属性,比如 close-on-exec 和 non-blocking 等。

三、radv 函数和 writev 函数

readv 函数将数据从文件描述符读到分散的内存块中,即分散读;writev 函数则将多块分散的内存数据一并写入文件描述符中,即集中写。它们的定义如下:

#include
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 函数。

四、sendfile 函数

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

#include
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 几乎是专门为在网络上传输文件而设计的。

五、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:控制内存段内容被修改后程序的行为。它可以被设置下表中的某些值(这里仅列出了常用的值)的按位或(其中
    MAP_SHARED 和 MAP_PRIVATE 是互斥的,不能同时指定):
    常用值 含义
    MAP_SHARED 在进程间共享这段内存,对该内存段的修改将反映到被映射的文件中,它提供了进程间共享内存的 POSIX 方法
    MAP_PRIVATE 内存段为调用进程所私有,对该内存段的修改不会反映到被映射的文件中
    MAP_ANONYMOUS 这段内存不是从文件映射而来的,其内容被初始化为全 0 ,这种情况下 mmap 函数的最后两个参数都将被忽略
    MAP_FIXED 内存段必须位于 start 参数指定的地址处,start 必须是内存页面大小(4096 字节)的整数倍
    MAP_HUGETLB 按照“大内存页面”来分配空间,可通过 /proc/meminfo 文件查看
  • fd:是被映射文件对应的文件描述符。它一般通过 open 系统调用 获得;
  • offset:设置从文件的何处开始映射(对于不需要读入整个文件的情况);
  • mmap 函数成功时返回指向目标内存区域的指针,失败则返回 MAP_FAILED((void*)-1)并设置 errno ;
  • munmap 函数成功时返回 0 , 失败则返回 -1 并设置 errno 。

六、splice 函数

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

#include 
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:控制数据如何移动,可被设置为下表中的某些值的按位或:
    常用值 含义
    SPLICE_F_MOVE 如果合适的话,按整页内存移动数据,因为存在 bug ,实际上没有任何效果
    SPLICE_F_NONBLOCK 非阻塞的 splice 操作,但实际效果还会受文件描述符本身的阻塞状态的影响
    SPLICE_F_MORE 给内核的一个提示:后续 splice 调用将读取更多数据
    SPLICE_F_GIFT 无效果
  • 使用 splice 函数时,fd_in 和 fd_out 必须至少有一个是管道文件描述符;
  • splice 函数调用成功时返回移动字节的数量。它可能返回 0 ,表示没有数据需要移动,这发生在从管道中读取数据(fd_in 是管道文件描述符)而该管道没有被写入任何数据时。splice 函数失败时返回 -1 并设置 errno 。常见的 errno 如下表所示:
    错误 含义
    EBADF 参数所指文件描述符有错
    EINVAL 目标文件系统不支持 splice ,或目标文件以追加方式打开,或者两个文件描述符都不是管道文件描述符,或者某个 offset 参数被用于不支持随机访问的设备
    ENOMEM 内存不够
    ESPIPE 参数 fd_in(或 fd_out)是管道文件描述符,而 off_in(或 off_out)不为 NULL 。

七、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 。

八、fcntl 函数

fcntl 函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制操作。另外一个常见的控制文件描述符属性和行为的系统调用是 ioctl ,而且 ioctl 比 fcntl 能够执行更多的控制。但是对于控制文件描述符常用的属性和行为,fcntl 函数是由 POSIX 规范指定的首选方法。所以本书仅讨论 fcntl 函数:

#include 
int fcntl(int fd,int cmd,...);
  • fd:被操作的文件描述符;
  • cmd:指定执行何种类型的操作。根据操作类型的不同,该函数可能还需要第三个可选参数 arg ;
  • fcntl 函数支持的常用操作如下:
    操作分类 操作 含义 第三个参数的类型 成功时的返回值
    复制文件描述符 F_DUPFD 创建一个新的文件描述符,其值大于或等于 arg long 新创建的文件描述符的值
    F_DUPFD_CLOEXEC 与 F_DUPFD 相似,不过在创建文件描述符的同时,设置其 close-on-exec 标志 long 新创建的文件描述符的值
    获取和设置文件描述符的标志 F_GETFD 获取 fd 的标志,比如 close-on-exec 标志 fd 的标志
    F_SETFD 设置 fd 的标志 long 0
    获取和设置文件描述符的状态标志 F_GETFL 获取 fd 的状态标志,这些标志包括可由 open 系统调用设置的标志(O_APPEND、O_CREAT 等)和访问模式(O_RDONLY、OWRONLY 和 O_RDWR) void fd 的标志
    F_SETFL 设置 fd 的状态标志,但部分标志是不能被修改的(比如访问模式标志) long 0
    管理信号 F_GETOWN 获得 SIGIO 和 SIGURG 信号的宿主进程的 PID 或进程组的组 ID 信号的宿主进程的 PID 或进程组的组 ID
    F_SETOWN 设定 SIGIP 和 SIGURG 信号的宿主进程的 PID 或者进程组的组 ID long 0
    F_GETSIG 获取当应用程序被通知 fd 可读或可写时,是哪个信号通知该事件的 信号值,0 表示 SIGIO
    F_SETSIG 设置当 fd 可读或可写时,系统应该出发哪个信号来通知应用程序 long 0
    操作管道容量 F_SETPIPE_SZ 设置由 fd 指定的管道的容量 long 0
    F_GETPIPE_SZ 获取由 fd 指定的管道的容量 管道容量
  • fcntl 函数成功时的返回值上表,失败则返回 -1 并设置 errno 。

在网络编程中,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;/*返回文件描述符旧的状态标志,以便*/ /*日后恢复该状态标志*/ 
}

此外,SIGIO 和 SIGURG 这两个信号与其他 Linux 信号不同,它们必须与某个文件描述符相关联方可使用:

  • 当被关联的文件描述符可读或可写时,系统将触发 SIGIO 信号;
  • 当被关联的文件描述符(而且必须是一个 socket )上有带外数据可读时,系统将触发 SIGURG 信号。

将信号和文件描述符关联的方法,就是使用 fcntl 函数为目标文件描述符指定宿主进程或进程组,那么被指定的宿主进程或进程组将捕获这两个信号。使用 SIGIO 时,还需要利用 fcntl 设置其 O_ASYNC 标志(异步 I/O 标 志,不过 SIGIO 信号模型并非真正意义上的异步 I/O 模型)。

你可能感兴趣的:(计算机网络,linux,网络,学习)