通过管道与C程序通信 | Communicating with C Programs via Pipes

章节概述

本章深入讲解 Bash 与 C 程序之间的管道通信机制,涵盖 stdin/stdout 管道、popen() 函数、从 Bash 读取 C 输出、向 Bash 传递 C 结果,以及进程替换 <() 的高级用法。管道是 Unix 哲学的核心,掌握它能让两种语言无缝协作。

核心理念:管道是进程间通信的瑞士军刀。通过管道,Bash 和 C 程序可以像流水线上的工人一样各司其职、高效协作。


第1节:stdin/stdout 管道基础

管道基本操作

# 基本管道:C 程序读取 stdin,输出到 stdout
echo "data" | ./my_program
 
# 多级管道
cat data.txt | ./parser | ./formatter | ./aggregator
 
# 管道 + 重定向
echo "data" | ./my_program > output.txt
echo "data" | ./my_program 2> error.log
echo "data" | ./my_program > output.txt 2>&1

C 程序管道示例

// word_count.c - 统计单词数量
#include <stdio.h>
#include <ctype.h>
 
int main() {
    int count = 0;
    int in_word = 0;
    int c;
 
    while ((c = getchar()) != EOF) {
        if (isspace(c)) {
            in_word = 0;
        } else if (!in_word) {
            in_word = 1;
            count++;
        }
    }
 
    printf("%d\n", count);
    return 0;
}
# 编译并使用管道
gcc -o word_count word_count.c
echo "Hello World from Bash" | ./word_count
# 输出: 4

管道数据流对照表

操作Bash 语法C 等价
读取 stdinread linefgets() / getchar()
输出 stdoutecho "msg"printf() / puts()
输出 stderrecho "err" >&2fprintf(stderr, ...)
管道输入cmd1 | cmd2stdin 重定向
管道输出cmd > filestdout 重定向

第2节:popen() 函数详解

popen() 基本用法

// popen_demo.c - 使用 popen 执行 Bash 命令
#include <stdio.h>
#include <stdlib.h>
 
int main() {
    char buffer[1024];
    FILE *pipe;
 
    // 执行命令并读取输出
    pipe = popen("ls -la /tmp", "r");
    if (pipe == NULL) {
        perror("popen failed");
        return 1;
    }
 
    while (fgets(buffer, sizeof(buffer), pipe) != NULL) {
        printf("%s", buffer);
    }
 
    int status = pclose(pipe);
    printf("Exit status: %d\n", WEXITSTATUS(status));
    return 0;
}

popen() 读写模式

模式用途示例
"r"读取命令输出popen("ls", "r")
"w"向命令写入数据popen("cat > file", "w")

C 程序使用 popen 写数据

// write_to_bash.c
#include <stdio.h>
 
int main() {
    FILE *pipe = popen("cat > /tmp/c_output.txt", "w");
    if (pipe == NULL) {
        perror("popen failed");
        return 1;
    }
 
    fprintf(pipe, "Line 1 from C\n");
    fprintf(pipe, "Line 2 from C\n");
    fprintf(pipe, "Line 3 from C\n");
 
    pclose(pipe);
    printf("Data written to file\n");
    return 0;
}

第3节:从Bash读取C输出

命令替换

# 基本命令替换
result=$(./my_program)
echo "C output: ${result}"
 
# 带参数的命令替换
result=$(./calc --expression "2+2")
echo "Result: ${result}"
 
# 捕获退出码
if output=$(./program 2>&1); then
  echo "Success: ${output}"
else
  echo "Failed with exit code: $?"
fi

多行输出处理

# C 程序输出多行
cat > multi_line.c << 'EOF'
#include <stdio.h>
int main() {
    printf("line1\n");
    printf("line2\n");
    printf("line3\n");
    return 0;
}
EOF
 
gcc -o multi_line multi_line.c
 
# Bash 处理多行输出
while IFS= read -r line; do
  echo "Processing: ${line}"
done < <(./multi_line)

逐行读取管道

# 方式1:while read
./my_program | while IFS= read -r line; do
  echo "Got: ${line}"
done
 
# 方式2:进程替换(保留变量)
while IFS= read -r line; do
  echo "Got: ${line}"
done < <(./my_program)
 
# 注意:while read 管道会在子 shell 中执行
# 变量不会保留到循环外
count=0
./my_program | while IFS= read -r line; do
  count=$((count + 1))
done
echo "Count: ${count}"  # 输出: Count: 0(变量未保留)

第4节:向Bash传递C结果

写文件方式

// write_result.c
#include <stdio.h>
#include <stdlib.h>
 
int main() {
    // 计算结果
    int result = 42;
    char *filename = "/tmp/result.txt";
 
    FILE *f = fopen(filename, "w");
    if (f == NULL) {
        perror("fopen failed");
        return 1;
    }
 
    fprintf(f, "%d\n", result);
    fclose(f);
 
    return 0;
}
gcc -o write_result write_result.c
./write_result
result=$(cat /tmp/result.txt)
echo "Got from C: ${result}"

使用文件描述符

// fd_communication.c
#include <stdio.h>
#include <unistd.h>
 
int main() {
    // 写入到 stdout
    printf("RESULT=%d\n", 42);
    return 0;
}
# 从 Bash 读取 C 程序的 stdout
while IFS='=' read -r key value; do
  case "${key}" in
    RESULT)
      echo "Got result: ${value}"
      ;;
  esac
done < <(./fd_communication)

第5节:进程替换 <() 高级用法

进程替换基本语法

# <() 将命令输出作为文件描述符
# 用于需要文件名参数的命令
 
# 示例1:比较两个命令的输出
diff <(./program1) <(./program2)
 
# 示例2:排序两个 C 程序的输出
sort <(./generator1) <(./generator2) > sorted.txt
 
# 示例3:同时处理多个 C 程序的输出
while IFS= read -r line; do
  echo "From A: ${line}"
done < <(./program_a)

进程替换 vs 管道

# 管道:输出进入下一个命令的 stdin
./program_a | ./program_b
 
# 进程替换:输出作为文件描述符
diff <(./program_a) <(./program_b)
 
# 管道限制:
# 1. 只能连接一个输入和一个输出
# 2. 数据流是单向的
# 3. 不能同时读取多个管道
 
# 进程替换优势:
# 1. 可以同时使用多个输入
# 2. 随机访问(某些场景)
# 3. 与需要文件名的命令兼容

实战示例:数据对比

// generator.c - 生成测试数据
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
int main(int argc, char *argv[]) {
    int seed = atoi(argv[1]);
    srand(seed);
    for (int i = 0; i < 10; i++) {
        printf("%d\n", rand() % 100);
    }
    return 0;
}
gcc -o generator generator.c
 
# 对比两组数据
echo "=== Data Set A vs B ==="
diff <(./generator 1) <(./generator 2) && echo "Identical" || echo "Different"
 
# 找出 A 中有但 B 中没有的数据
comm -23 <(./generator 1 | sort) <(./generator 2 | sort)
 
# 合并并排序
sort -n <(./generator 1) <(./generator 2) | uniq

第6节:高级管道模式

双向管道(使用 FIFO)

# 创建命名管道
mkfifo /tmp/input_pipe
mkfifo /tmp/output_pipe
 
# C 程序从 input_pipe 读取,写入 output_pipe
# Bash 写入 input_pipe,从 output_pipe 读取
 
# 启动 C 程序(后台)
./bidirectional_program &
 
# Bash 发送数据
echo "request" > /tmp/input_pipe
 
# Bash 读取响应
read response < /tmp/output_pipe
echo "Got: ${response}"
 
# 清理
rm -f /tmp/input_pipe /tmp/output_pipe

带缓冲的管道通信

#!/usr/bin/env bash
set -euo pipefail
 
# 创建临时文件和管道
TEMP_IN=$(mktemp)
TEMP_OUT=$(mktemp)
mkfifo "${TEMP_IN}.fifo"
 
# 后台运行 C 程序
./server < "${TEMP_IN}.fifo" > "${TEMP_OUT}" &
SERVER_PID=$!
 
# 通过管道发送请求
exec 3>"${TEMP_IN}.fifo"
echo "REQUEST:status" >&3
exec 3>&-
 
# 读取响应
read -r response < "${TEMP_OUT}"
echo "Server response: ${response}"
 
# 清理
kill "${SERVER_PID}" 2>/dev/null || true
rm -f "${TEMP_IN}" "${TEMP_OUT}" "${TEMP_IN}.fifo"

管道错误处理

#!/usr/bin/env bash
set -euo pipefail
 
# 检查管道中每个命令的退出码
pipe_with_error_check() {
  local cmd1_exit=0 cmd2_exit=0
 
  ./program_a || cmd1_exit=$?
  ./program_b || cmd2_exit=$?
 
  if [ ${cmd1_exit} -ne 0 ]; then
    echo "Program A failed with exit code: ${cmd1_exit}" >&2
    return 1
  fi
 
  if [ ${cmd2_exit} -ne 0 ]; then
    echo "Program B failed with exit code: ${cmd2_exit}" >&2
    return 1
  fi
}
 
# 使用 set -o pipefail
set -o pipefail
./program_a | ./program_b | ./program_c
# 如果任何命令失败,整个管道返回非零

常见管道错误对照

错误原因解决方案
变量未保留while read 在子 shell使用 < <(cmd) 进程替换
管道断裂SIGPIPE 信号设置 set -o pipefail
读取阻塞C 程序未关闭 stdout确保 C 程序正常退出
编码问题UTF-8 不匹配设置 LC_ALL=C
缓冲问题C 程序使用缓冲输出使用 fflush(stdout)

本节帮助你掌握 Bash 与 C 程序的管道通信,实现高效的数据流转和进程协作。