pipe函数 pipe函数可用于创建一个管道,以实现进程间通信(例如父子进程间通信)。pipe函数的定义如下:
1 2 #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]则只能用于往管道写入数据,而不能反过来使用,如果要实现双向的数据传输,就应该使用两个管道。
另外,默认情况下,这一对文件描述符都是阻塞的 。如果我们用read
系统调用来读取一个空的管道,则read将被阻塞,直到管道内有数据可读,如果我们用write系统调用来读取一个空的管道,则read
将被阻塞,同理如果用write
系统调用来往一个满的管道中写入数据,则write
也将被阻塞,直到管道有足够多的空闲空间可用。
如果管道的写端文件描述符fd[1]的引用计数减少至0,即没有任何进程需要往管道中写入数据,则针对管道的读端文件描述符fd[0]的read操作将返回0,即读取到了文件结束标记(End of File, EOF);反之,如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对管道的写端文件描述符fd[1]的write操作将失败,并引发SIGPIPE
信号。
管道内传输的数据是字节流,管道本身有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少个字节的数据。 自Linux2.6.11内核起,管道容量的大小默认是65536字节。我们可以使用fcntl
函数来修改管道容量。
其实shell命令中的管道操作的实现也是通过pipe函数来实现的。
pipe的实现原理,其实是在操作系统内核中开辟了一个缓冲区(位于内存),然后让返回的两个文件描述符都指向这个内核缓存区,然后设置一个文件描述符只能读,一个文件描述符只能写。写pipe时需要将数据从用户空间拷贝到内核缓冲区,读数据时需要将数据从内核缓冲区拷贝到用户空间。
pipe只能用在两个有亲缘关系的进程上,例如父子进程;如果要在两个没有关系的进程上用管道通信,需要使用fifo命名管道,FiFo命名管道利用了磁盘文件。
dup函数和dup2函数 有时候我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接,这可以通过下面的用于复制文件描述符的dup或者dup2函数来实现。
1 2 3 #include <unistd.h> int dup (inf fd) ;int dup2 (int fd_one, int fd_two) ;
dup函数创建一个新的文件描述符,该文件描述符和原有文件描述符fd指向相同的文件、管道或者网络连接。并且dup返回的文件描述符总是取系统当前可用的最小整数值
。dup2和dup类型,不过它将返回第一个不小于(大于等于) fd_two的整数值。dup和dup2系统调用失败时返回-1并设置errno。
利用dup函数实现将标准输出重定向到一个网络连接中:
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 #include <iostream> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main () { sockaddr_in sockAddress{}; sockAddress.sin_family = AF_INET; inet_pton (AF_INET, "localhost" , &sockAddress.sin_addr); sockAddress.sin_port = htons (5000 ); int sockFD = socket (PF_INET, SOCK_STREAM, 0 ); if (sockFD < 0 ) { std::cerr << "create socket fail" << std::endl; return sockFD; } int ret = bind (sockFD, (sockaddr *)&sockAddress, sizeof (sockAddress)); if (ret < 0 ) { std::cerr << "socket bind fail" << std::endl; return ret; } ret = listen (sockFD, 5 ); if (ret < 0 ) { std::cerr << "socket listen fail" << std::endl; return ret; } sockaddr_in clientSock{}; socklen_t clientSockLen; int clientSockFD = accept (sockFD, (sockaddr *)&clientSock, &clientSockLen); if (clientSockFD < 0 ) { std::cerr << "socket accept fail" << std::endl; return clientSockFD; } else { close (STDOUT_FILENO); int newFd = dup (clientSockFD); std::cout << "new fd is equal to std out fd: " << (newFd == STDOUT_FILENO) << std::endl; std::cout << "this is direct from std out" << std::endl; close (clientSockFD); close (newFd); } close (sockFD); return 0 ; }
readv函数和writev函数 readv函数将数据从磁盘读到分散的内存块中,即分散读;wirtev函数则将多块分散的内存数据一并写入文件描述符中,即集中写。它们的定义如下:
1 2 3 #include <sys/uio.h> ssize_t readv (int fd, const struct iovec* vector , int count) ;ssize_t writev (int fd, const struct iovect* vector , int count) ;
struct iovect用来描述一块内存区,它的定义如下:
1 2 3 4 struct iovec { void *iov_base; size_t iov_len; }
sendfile函数 sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作
),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝
。sendfile函数的定义如下:
1 2 #include <sys/sendfile.h> 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。
sendfile函数有一个限制:in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket管道;而out_fd则必须是一个socket。 由此可见,sendfile几乎是专门为在网络上传输文件而设计的。
# mmap、munmap、msync函数
mmap函数用于申请一段内存空间。`我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中`。munmap函数则释放由mmap创建的这段内存空间。它们定义如下:
1 2 3 #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_ANONYMOUS,这段内存不是从文件映射来的。其内容被初始化为全0。这种情况下,mmap函数的最后两个参数将被忽略
* MAP_FIXED,内存段必须位于start参数指定的地址处,
start必须是内存页面大小(4096字节)的整数倍 ,考虑到可移植性,addr 通常设为 NULL ,不指定 MAP_FIXED
* MAP_HUGETLB,按照“大内存页面”来分配内存空间,“大内存页面”的大小可通过/proc/meminfo文件来查看
* fd参数是被映射文件对应的文件描述符。它一般通过open系统调用来获得
* offset参数设置从文件的何处开始映射
mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED,并设置errno。munmap函数成功时返回0,失败时返回-1并设置errno。`当 mmap 成功返回时,fd 就可以关闭,这并不影响创建的映射区。`
`进程退出的时候,映射区会自动删除`。不过当不再需要映射区时,可以调用 munmap 显式删除。当映射区删除后,后续对映射区的引用会生成 SIGSEGV 信号。
文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回的地址空间的操作只在内存中有意义。并且只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件。
## linux内存映射mmap原理
在调用mmap函数的时候,实际上只是创建并初始化了相关的结构体,这个结构体中记录了从`逻辑地址`到磁盘空间的映射,`此时并没有发生任何磁盘数据的传输`。
当使用mmap返回的逻辑地址对数据进行访问时,操作系统将逻辑地址转换为物理地址,如果发现页表项中没有对应的物理页时,此时就会根据mmap建立的映射关系,从磁盘中间对应数据加载到物理页中,`这个物理页是属于用户空间的物理页`
`为什么使用mmap会比使用普通的read/write快?`先看看使用普通的read/write的过程,当使用read/write函数从某个fd上读取数据时,操作系统首先从磁盘将数据加载到属于内核的`物理地址`中,然后需要将这些数据从属于内核的`物理地址`拷贝到属于用户空间的`物理地址`中,此时才会在用户空间中获取到这些数据。这个过程经过了两次数据拷贝的过程。而使用mmap,会直接把数据从磁盘中拷贝到属于用户的物理空间中,只经过了一次数据拷贝的操作。
# splice函数
splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。splice函数的定义如下:
1 2 #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) ;
* fd_in参数是需要读数据的文件描述符。
* off_in表示从输入数据流的何处开始读取数据,如果fd_in是一个管道文件描述符,那么off_in参数必须被设置为NULL,表示从输入数据流的当前偏移位置开始读取数据。
* fd_out表示需要写数据的文件描述符。
* off_out表示从何处开始写数据,如果fd_out是一个管道文件描述符,那么off_out参数必须设置为NULL,表示从当前偏移位置开始写数据。
* len参数表示需要拷贝数据的长度
* flags参数控制数据如何移动,它可以被设置为下列这些值的按位或
* SPLICE_F_MOVE:如果合适的话,按整页内存移动数据。这只是给内核一个提示。不过,因为它的实现存在BUG,自内核2.6.21后,它实际上没有任何效果。
* SPLICE_F_NONBLOCK:非阻塞的splice操作,
但实际效果还会受文件描述符本身的阻塞状态的影响
* SPLICE_F_MORE:给内核一个提示:后续的splice调用将读取更多的数据
* SPLICE_F_GIFT:对splice没有效果
使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符,但因为管道文件有大小限制,所以splice函数一次移动太多数据可能会导致长时间阻塞
tee函数 tee
函数在两个管道 文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。 tee
函数的原型如下:
1 2 #include <fcntl.h> ssize_t tee (int fd_in, int fd_out, size_t len, unsigned int flags) ;
tee
函数的参数的含义和splice
函数相同,只不过fd_in和fd_out必须都是管道文件描述符。
fcntl函数 fcntl函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制操作。另外一个常见的控制文件描述符属性和行为的系统调用是ioctl,而且ioctl比fcntl能够执行更多的操作。但是,对于控制文件描述符常用的属性和行为,fcntl函数是有POSIX规范指定的首选方法。fcntl函数的定义如下:
1 2 #include <fcntl.h> int fcntl (int fd, int cmd, ...) ;
fd参数是被操作的文件描述符,cmd参数指定执行何种类型的操纵。根据操作类型的不同,该函数可能还需要第三个可选参数arg
。fcnt函数支持的常用操作及其参数如下表所示:
操作分类
操纵
含义
第三个参数类型
成功时的返回值
复制文件描述
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、O_WRONLY和O_RDWR)
void
fd的状态标志
F_SETFL
设置文件的状体标志,但部分标志是不能被修改的(比如访问模式标志)
long
0
管理信号
F_GETOWN
获得SIGIO和SIGURG信号的宿主进程的PID或进程组的组ID
无
信号的宿主进程的PID或进程组的组ID
F_SETOWN
设定SIGIO和SIGURG信号的宿主进程的PID或者进程组的组ID
long
0
F_GETSIG
获取当应用程序被通知fd可读可写时,是那个信号通知该事件的
无
信号值,0表示SIGIO
F_SETSIG
设置当fd可读或可写时,系统应该触发哪个信号来通知应用程序
long
0
操作管道容量
F_SETPIPE_SZ
设置由fd指定的管道的容量,/proc/sys/fs/pipe-size-max内核参数指定了fcntl能设置的管道容量的上限。
long
0
F_GETPIPE_SZ
获取由fd指定的管道的容量
无
管道容量
在网络编程中,fcntl函数通常用来将一个socket文件描述符设置为非阻塞的:
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信号不同,他们必须与某个文件描述符相关联才能使用 ,当被关联的文件描述符可读或可写时,系统将触发SIGIIO信号,当被关联的文件描述符(而且必须是一个socket)上有带外数据可读时,系统将触发SIGURG信号,
将信号和文件描述符关联的方法,就是使用fcntl函数为目标文件描述符指定宿主进程或进程组,那么被指定的宿主进程或进程组将捕获这两个信号。