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 。
有时我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接(比如 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 等。
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);
sendfile 函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。sendfile 函数的定义如下:
#include
ssize_t sendfile(int out_fd,int in_fd,off_t*offset,size_t count);
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);
常用值 | 含义 |
---|---|
MAP_SHARED | 在进程间共享这段内存,对该内存段的修改将反映到被映射的文件中,它提供了进程间共享内存的 POSIX 方法 |
MAP_PRIVATE | 内存段为调用进程所私有,对该内存段的修改不会反映到被映射的文件中 |
MAP_ANONYMOUS | 这段内存不是从文件映射而来的,其内容被初始化为全 0 ,这种情况下 mmap 函数的最后两个参数都将被忽略 |
MAP_FIXED | 内存段必须位于 start 参数指定的地址处,start 必须是内存页面大小(4096 字节)的整数倍 |
MAP_HUGETLB | 按照“大内存页面”来分配空间,可通过 /proc/meminfo 文件查看 |
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);
常用值 | 含义 |
---|---|
SPLICE_F_MOVE | 如果合适的话,按整页内存移动数据,因为存在 bug ,实际上没有任何效果 |
SPLICE_F_NONBLOCK | 非阻塞的 splice 操作,但实际效果还会受文件描述符本身的阻塞状态的影响 |
SPLICE_F_MORE | 给内核的一个提示:后续 splice 调用将读取更多数据 |
SPLICE_F_GIFT | 无效果 |
错误 | 含义 |
---|---|
EBADF | 参数所指文件描述符有错 |
EINVAL | 目标文件系统不支持 splice ,或目标文件以追加方式打开,或者两个文件描述符都不是管道文件描述符,或者某个 offset 参数被用于不支持随机访问的设备 |
ENOMEM | 内存不够 |
ESPIPE | 参数 fd_in(或 fd_out)是管道文件描述符,而 off_in(或 off_out)不为 NULL 。 |
tee 函数在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作:
#include<fcntl.h>
ssize_t tee(int fd_in,int fd_out,size_t len,unsigned int flags);
fcntl 函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制操作。另外一个常见的控制文件描述符属性和行为的系统调用是 ioctl ,而且 ioctl 比 fcntl 能够执行更多的控制。但是对于控制文件描述符常用的属性和行为,fcntl 函数是由 POSIX 规范指定的首选方法。所以本书仅讨论 fcntl 函数:
#include
int fcntl(int fd,int cmd,...);
操作分类 | 操作 | 含义 | 第三个参数的类型 | 成功时的返回值 |
---|---|---|---|---|
复制文件描述符 | 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 函数通常用来将一个文件描述符设置为非阻塞的:
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 信号不同,它们必须与某个文件描述符相关联方可使用:
将信号和文件描述符关联的方法,就是使用 fcntl 函数为目标文件描述符指定宿主进程或进程组,那么被指定的宿主进程或进程组将捕获这两个信号。使用 SIGIO 时,还需要利用 fcntl 设置其 O_ASYNC 标志(异步 I/O 标 志,不过 SIGIO 信号模型并非真正意义上的异步 I/O 模型)。