Linux提供了很多高级的I/O函数。它们并不像Linux基础I/O函数(比如open和read)那么常用(编写内核模块时一般要实现这些I/O函数),但在特定的条件下却表现出优秀的性能。这些函数大致分为三类:
用于创建文件描述符的函数,包括pipe、socketpair、dup/dup2函数。
用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee函数。
用于控制I/O行为和属性的函数,包括fcntl函数。
本节先介绍第一类
一、pipe函数
pipe
函数用于创建一个管道,以实现进程间通信。
c
#include <unistd.h>
int pipe(int fd[2]);
pipe函数创建的这两个文件描述符fd[0]和fd[1]分别构成管道的两端,往fd[1]写入的数据可以从fd[0]读出。并且,fd[0]只能用于从管道读出数据,fd[1]则只能用于往管道写入数据,而不能反过来使用。如果要实现双向的数据传输,就应该使用两个管道。
默认情况下,这一对文件描述符都是阻塞的,若用read来读取一个空的管道,则read将被阻塞,直到管道内有数据可读。如果应用程序将fd[0]和fd[1]都设置为非阻塞的,则read和write会有不同的行为。
-
如果管道的写端文件描述符fd[1]的引用计数减少至0,即没有任何进程需要往管道中写入数据,则针对该管道的读端文件描述符fd[0]的read操作将返回0,即读取到了文件结束标记(End Of File,EOF);
-
如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符fd[1]的write操作将失败,并引发SIGPIPE信号。
管道容量的大小默认是65536字节。我们可以使用fcntl函数来修改管道容量。
下面举一个在多进程程序中管道通信的例子:
c
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int pipefd[2];
char buf[1024];
// 创建管道
if (pipe(pipefd) == -1)
{
perror("pipe error! \n");
return 1;
}
for (int i = 0; i < 8; i++)
{
// 创建子进程
pid_t pid = fork();
if (pid == -1)
{
perror("fork error!\n");
return 1;
}
else if (pid == 0)
{
// 子进程关闭写端
close(pipefd[1]);
// 从管道读取数据
memset(buf, 0, 1024);
read(pipefd[0], buf, sizeof(buf));
// 打印数据
printf("pid = %d :Child process received: %s\n", getpid(), buf);
// 子进程关闭读取端
close(pipefd[0]);
// 子进程直接break,以免创建更多的子进程
break;
}
}
if (getpid() != 0)
{
// 父进程关闭读取端
close(pipefd[0]);
// 向管道写入数据
const char *message = "Hello from parent process";
for (int i = 0; i < 8; i++)
{
write(pipefd[1], message, strlen(message));
sleep(1);
}
// 在所有数据都写入后再关闭写入端
close(pipefd[1]);
}
return 0;
}
生成了八个子进程,八个子进程阻塞在read处,等待父进程的消息,父进程发了八次消息,每次间隔1秒。看一下仿真
二、socketpair函数
socketpair可以用于创建一堆相互连接的套接字,通常用于在进程之间通信
c
int socketpair(int domain, int type, int protocol, int sv[2]);
参数说明:
domain
:指定地址族,通常为AF_UNIX
表示 UNIX 域套接字。type
:指定套接字类型,通常为SOCK_STREAM
表示面向连接的套接字。protocol
:指定使用的协议,通常为 0 表示默认协议。sv[2]
:用于存放创建的两个套接字的文件描述符的数组。
下面举个多进程的例子
c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int socketfd[2];
char buf[1024];
// 创建一对相互连接的套接字
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socketfd) == -1) {
perror("socketpair");
return -1;
}
for (int i = 0; i < 8; i++)
{
// 创建子进程
pid_t pid = fork();
if (pid == -1)
{
perror("fork error!\n");
return 1;
}
else if (pid == 0)
{
// 子进程关闭写端
close(socketfd[1]);
// 从管道读取数据
memset(buf, 0, 1024);
read(socketfd[0], buf, sizeof(buf));
// 打印数据
printf("pid = %d :Child process received: %s\n", getpid(), buf);
// 子进程关闭读取端
close(socketfd[0]);
// 子进程直接break,以免创建更多的子进程
break;
}
}
if (getpid() != 0)
{
// 父进程关闭读取端
close(socketfd[0]);
// 向管道写入数据
const char *message = "Hello from parent process";
for (int i = 0; i < 8; i++)
{
write(socketfd[1], message, strlen(message));
sleep(1);
}
// 在所有数据都写入后再关闭写入端
close(socketfd[1]);
}
return 0;
}
例子和上面的 pipe
是一样的,看仿真:
三、dup函数和dup2函数
dup函数和dup2函数可以把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接
c
#include <unistd.h>
int dup(int file_descriptor);
int dup2(int oldfd, int newfd);
dup
函数创建一个新的文件描述符,该新文件描述符和原有文件描述符file_descriptor
指向相同的文件、管道或者网络连接。并且dup返回的文件描述符总是取系统当前可用的最小整数值。
dup2
函数可以将 oldfd
重定向到 newfd
。如果newfd已经被程序使用,则系统会先将newfd所指的文件关闭。
**注意:**通过dup和dup2创建的文件描述符并不继承原文件描述符的属性,比如close-on-exec
和non-blocking
等。
3.1、使用 dup2
将标准输出重定向到一个文件
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main() {
// 打开或创建文件,以便将标准输入内容写入到该文件中
int file_fd = open("output.txt", O_RDWR | O_CREAT, 0666);
if (file_fd == -1) {
perror("open");
return 1;
}
// 将标准输出重定向到文件描述符指向的文件
dup2(file_fd, STDOUT_FILENO)
printf("newfd: hello, world\n");
const char* buf = "oldfd: hello, world\n";
write(file_fd, buf, strlen(buf));
close(file_fd);
return 0;
}
将标准输出重定向到 output.txt
文件,所以printf
打印函数打印的字符串直接输出到了文件中,而不是控制台上。
3.2、使用 dup2
将标准输入重定向到一个文件
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main() {
// 打开或创建文件,以便将标准输入内容写入到该文件中
int file_fd = open("output.txt", O_RDWR | O_CREAT, 0666);
if (file_fd == -1) {
perror("open");
return 1;
}
dup2(file_fd, STDIN_FILENO)
char buff[1024] = {0};
read(STDIN_FILENO,buff,1024);
printf("%s",buff);
close(file_fd);
return 0;
}
相当于直接从文件中读取数据作为标准输入了
3.2、使用 dup
将标准输出重定向到一个文件
c
int main() {
// 打开或创建文件,以便将标准输入内容写入到该文件中
int file_fd = open("output.txt", O_RDWR | O_CREAT, 0666);
if (file_fd == -1) {
perror("open");
return 1;
}
close(STDOUT_FILENO);
dup(file_fd);
printf("123");
close(file_fd);
return 0;
}