tee函数详解 Link to heading

1. 函数介绍 Link to heading

tee函数是Linux系统中一个非常实用的函数,它的作用就像现实中的"T型管道"一样。想象一下水管系统中的T型接头,水从主管道流入,同时流向两个不同的分支管道。在Linux编程中,tee函数就是这样一个"管道分流器",它能够将一个管道中的数据同时复制到另一个管道中,而不需要将数据从内核空间复制到用户空间再复制回去。

使用场景:

  • 管道数据的复制和分流
  • 避免不必要的数据拷贝,提高程序性能
  • 实现数据的并行处理
  • 日志记录系统中同时写入多个输出流

2. 函数原型 Link to heading

#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

3. 功能 Link to heading

tee函数的主要功能是在两个管道描述符之间高效地复制数据。它不经过用户空间,直接在内核中完成数据的复制,这大大提高了效率。这个函数特别适用于需要将数据从一个管道同时发送到多个目的地的场景。

4. 参数 Link to heading

  • fd_in: 源管道文件描述符(输入端)

    • 类型:int
    • 含义:数据来源的管道描述符,必须是管道或套接字
  • fd_out: 目标管道文件描述符(输出端)

    • 类型:int
    • 含义:数据目标的管道描述符,必须是管道或套接字
  • len: 要复制的数据长度

    • 类型:size_t
    • 含义:希望复制的最大字节数
  • flags: 操作标志

    • 类型:unsigned int
    • 含义:控制复制行为的标志位
    • 常用值:
      • SPLICE_F_MOVE:尽可能移动页面而不是复制
      • SPLICE_F_NONBLOCK:非阻塞操作
      • SPLICE_F_MORE:提示还有更多数据要写入
      • SPLICE_F_GIFT:页面是礼品(内核内部使用)

5. 返回值 Link to heading

  • 成功: 返回实际复制的字节数(ssize_t类型)
  • 失败: 返回-1,并设置errno错误码
    • EINVAL:参数无效
    • EBADF:文件描述符无效
    • ESPIPE:文件描述符不是管道
    • ENOMEM:内存不足

6. 相似函数或关联函数 Link to heading

  • splice(): 在文件描述符之间移动数据
  • vmsplice(): 从用户空间缓冲区向管道写入数据
  • read()/write(): 传统的数据读写函数
  • pipe(): 创建管道

7. 示例代码 Link to heading

示例1:基础tee使用 - 简单的数据分流 Link to heading

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

int main() {
    int pipe1[2], pipe2[2];  // 创建两个管道
    char buffer1[100], buffer2[100];
    ssize_t bytes_written, bytes_read;
    
    // 创建管道1
    if (pipe(pipe1) == -1) {
        perror("创建管道1失败");
        exit(EXIT_FAILURE);
    }
    
    // 创建管道2
    if (pipe(pipe2) == -1) {
        perror("创建管道2失败");
        exit(EXIT_FAILURE);
    }
    
    // 向管道1写入数据
    const char* message = "Hello, tee function!";
    write(pipe1[1], message, strlen(message));
    
    // 使用tee函数将管道1的数据复制到管道2
    // 注意:tee不消耗数据,数据仍保留在源管道中
    bytes_written = tee(pipe1[0], pipe2[1], strlen(message), SPLICE_F_NONBLOCK);
    
    if (bytes_written == -1) {
        perror("tee函数执行失败");
        exit(EXIT_FAILURE);
    }
    
    printf("tee函数复制了 %zd 字节的数据\n", bytes_written);
    
    // 从管道1读取数据(验证数据仍然存在)
    bytes_read = read(pipe1[0], buffer1, sizeof(buffer1) - 1);
    if (bytes_read > 0) {
        buffer1[bytes_read] = '\0';
        printf("从管道1读取: %s\n", buffer1);
    }
    
    // 从管道2读取数据(验证数据已成功复制)
    bytes_read = read(pipe2[0], buffer2, sizeof(buffer2) - 1);
    if (bytes_read > 0) {
        buffer2[bytes_read] = '\0';
        printf("从管道2读取: %s\n", buffer2);
    }
    
    // 关闭所有文件描述符
    close(pipe1[0]);
    close(pipe1[1]);
    close(pipe2[0]);
    close(pipe2[1]);
    
    return 0;
}

示例2:tee实现日志同时输出到文件和终端 Link to heading

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

int main() {
    int log_pipe[2], stdout_pipe[2];
    int log_file;
    ssize_t bytes_copied;
    const char* log_message = "这是一个重要的日志信息\n";
    
    // 创建管道用于日志处理
    if (pipe(log_pipe) == -1) {
        perror("创建日志管道失败");
        exit(EXIT_FAILURE);
    }
    
    // 创建管道用于标准输出
    if (pipe(stdout_pipe) == -1) {
        perror("创建标准输出管道失败");
        exit(EXIT_FAILURE);
    }
    
    // 打开日志文件
    log_file = open("app.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (log_file == -1) {
        perror("打开日志文件失败");
        exit(EXIT_FAILURE);
    }
    
    // 向日志管道写入消息
    write(log_pipe[1], log_message, strlen(log_message));
    
    // 使用tee将日志同时复制到标准输出管道和日志文件
    // 第一次tee:复制到标准输出管道
    bytes_copied = tee(log_pipe[0], stdout_pipe[1], strlen(log_message), SPLICE_F_MORE);
    if (bytes_copied == -1) {
        perror("tee复制到标准输出失败");
        exit(EXIT_FAILURE);
    }
    
    // 第二次tee:复制到日志文件(使用splice,因为文件不是管道)
    // 注意:tee只能用于管道之间,文件需要使用splice
    bytes_copied = splice(log_pipe[0], NULL, log_file, NULL, strlen(log_message), SPLICE_F_MOVE);
    if (bytes_copied == -1) {
        perror("splice复制到日志文件失败");
        exit(EXIT_FAILURE);
    }
    
    // 从标准输出管道读取并显示
    char output_buffer[256];
    ssize_t bytes_read = read(stdout_pipe[0], output_buffer, sizeof(output_buffer) - 1);
    if (bytes_read > 0) {
        output_buffer[bytes_read] = '\0';
        printf("终端输出: %s", output_buffer);
    }
    
    printf("日志已同时写入文件和终端\n");
    
    // 清理资源
    close(log_pipe[0]);
    close(log_pipe[1]);
    close(stdout_pipe[0]);
    close(stdout_pipe[1]);
    close(log_file);
    
    return 0;
}

示例3:tee与管道链结合使用 Link to heading

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int pipe1[2], pipe2[2], pipe3[2];
    pid_t pid1, pid2;
    
    // 创建三个管道
    if (pipe(pipe1) == -1 || pipe(pipe2) == -1 || pipe(pipe3) == -1) {
        perror("创建管道失败");
        exit(EXIT_FAILURE);
    }
    
    // 创建第一个子进程 - 数据生产者
    pid1 = fork();
    if (pid1 == 0) {
        // 子进程1:向管道1写入数据
        close(pipe1[0]); // 关闭读端
        const char* data = "数据流: 1 2 3 4 5\n";
        write(pipe1[1], data, strlen(data));
        close(pipe1[1]);
        exit(0);
    }
    
    // 创建第二个子进程 - 数据处理和分流
    pid2 = fork();
    if (pid2 == 0) {
        // 子进程2:使用tee分流数据
        close(pipe1[1]); // 关闭管道1的写端
        close(pipe2[0]); // 关闭管道2的读端
        close(pipe3[0]); // 关闭管道3的读端
        
        // 使用tee将管道1的数据同时复制到管道2和管道3
        ssize_t copied = tee(pipe1[0], pipe2[1], 1024, SPLICE_F_MORE);
        if (copied > 0) {
            // 再次tee复制到第三个管道
            tee(pipe1[0], pipe3[1], 1024, 0);
        }
        
        close(pipe1[0]);
        close(pipe2[1]);
        close(pipe3[1]);
        exit(0);
    }
    
    // 父进程:读取分流后的数据
    close(pipe1[0]); close(pipe1[1]); // 父进程不需要管道1
    close(pipe2[1]); // 父进程不需要管道2的写端
    close(pipe3[1]); // 父进程不需要管道3的写端
    
    // 等待子进程完成
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);
    
    // 读取管道2的数据
    char buffer2[256];
    ssize_t bytes2 = read(pipe2[0], buffer2, sizeof(buffer2) - 1);
    if (bytes2 > 0) {
        buffer2[bytes2] = '\0';
        printf("处理器1收到: %s", buffer2);
    }
    
    // 读取管道3的数据
    char buffer3[256];
    ssize_t bytes3 = read(pipe3[0], buffer3, sizeof(buffer3) - 1);
    if (bytes3 > 0) {
        buffer3[bytes3] = '\0';
        printf("处理器2收到: %s", buffer3);
    }
    
    // 清理资源
    close(pipe2[0]);
    close(pipe3[0]);
    
    return 0;
}

编译和运行 Link to heading

# 编译示例1
gcc -o tee_example1 tee_example1.c
./tee_example1

# 编译示例2
gcc -o tee_example2 tee_example2.c
./tee_example2

# 编译示例3
gcc -o tee_example3 tee_example3.c
./tee_example3

通过这些示例,你可以看到tee函数在数据分流、日志处理和管道链中的强大应用。记住,tee函数的关键优势是避免了用户空间和内核空间之间的数据拷贝,这在处理大量数据时能显著提高性能。