问题全过程


代码

先上代码看情况

正常代码:

```c unwrap #include “kernel/types.h” #include “user/user.h”

#define MSG “ping” #define REPLY “pong”

int main(int argc, char *argv[]) { int pipe1[2]; // parent -> child int pipe2[2]; // child -> parent char buf[10];

if (pipe(pipe1) < 0 || pipe(pipe2) < 0) { fprintf(2, “pipe failed\n”); exit(1); } int pid = fork();

if (pid < 0) { fprintf(2, “fork failed\n”); exit(1); } else if (pid == 0) { // child process close(pipe1[1]); // close write end of pipe1 close(pipe2[0]); // close read end of pipe2

// send message and receive message
int n = read(pipe1[0], buf, strlen(MSG));
buf[n] = '\0'; // null-terminate the string
if (n + 1 != strlen(MSG) + 1) {
  fprintf(2, "child: read error\n");
  exit(1);

}
printf("[child] received: %s\n", buf);

write(pipe2[1], REPLY, strlen(REPLY)); // send pong
printf("[child] sent: %s\n", REPLY);

close(pipe1[0]);
close(pipe2[1]);
exit(0);   } else {
// parent process
close(pipe1[0]); // close read end of pipe1
close(pipe2[1]); // close write end of pipe2

printf("[parent] sent: %s\n", MSG);
write(pipe1[1], MSG, strlen(MSG)); // send ping

// Wait for the child to finish(otherwise parent output is out of order)
wait(0);

int n = read(pipe2[0], buf, strlen(REPLY));
buf[n] = '\0'; // null-terminate the string
if (n + 1 != strlen(MSG) + 1) {
  fprintf(2, "child: read error\n");
  exit(1);

}

printf("[parent] received: %s\n", buf);

close(pipe1[1]);
close(pipe2[0]);   }   return 0; } ```

不正常代码:

```c unwrap #include “kernel/types.h” #include “user/user.h”

#define MSG “ping” #define REPLY “pong”

int main(int argc, char *argv[]) { int pipe1[2]; // parent -> child int pipe2[2]; // child -> parent char buf[10];

if (pipe(pipe1) < 0 || pipe(pipe2) < 0) { fprintf(2, “pipe failed\n”); exit(1); } int pid = fork();

if (pid < 0) { fprintf(2, “fork failed\n”); exit(1); } else if (pid == 0) { // child process close(pipe1[1]); // close write end of pipe1 close(pipe2[0]); // close read end of pipe2

// send message and receive message
read(pipe1[0], buf, sizeof(buf));
printf("[child] received: %s\n", buf);

write(pipe2[1], REPLY, strlen(REPLY) + 1); // send pong
printf("[child] sent: %s\n", REPLY);

close(pipe1[0]);
close(pipe2[1]);
exit(0);   } else {
// parent process
close(pipe1[0]); // close read end of pipe1
close(pipe2[1]); // close write end of pipe2

printf("[parent] sent: %s\n", MSG);
write(pipe1[1], MSG, strlen(MSG) + 1); // send ping

// Wait for the child to finish
wait(0);

read(pipe2[0], buf, sizeof(buf));
printf("[parent] received: %s\n", buf);

close(pipe1[1]);
close(pipe2[0]);   }   return 0; } ```

很显然, 问题出在 read 这里的参数有些不一样, 一个是 sizeof ,一个是 strlen

从而导致一个输出是:

```text unwrap [parent] sent: ping [child] received: ping [child] sent: pong [parent] received: pong


```text unwrap
[parent] sent: ping
[child] received: 
[child] sent: pong
[parent] received: pong

这样的怪象, 但是上面两份代码都在正常 linux 环境下运行 (简单修改头文件这些) 都可以正常输出, 那么问题到底是在哪里呢?🤔


点明 AI 破局

直接和 AI 说明问题可能是 sizeofstrlen 的问题之后一步一步排查出错误.

通过以下调用路径就可以查找出问题:

```text unwrap read └── sys_read └── fileread └── piperead


我已经找到了问题的根源,它在 `kernel/pipe.c` 的 `piperead` 函数中。

### 1. `piperead` 核心代码

```c
int
piperead(struct pipe *pi, uint64 addr, int n)
{
  int i;
  struct proc *pr = myproc();
  char ch;

  acquire(&pi->lock);
  // 如果管道为空,并且写端是打开的,就一直睡眠等待
  while(pi->nread == pi->nwrite && pi->writeopen){
    if(killed(pr)){
      release(&pi->lock);
      return -1;
    }
    sleep(&pi->nread, &pi->lock);
  }

  // 从管道中拷贝数据
  for(i = 0; i < n; i++){  // <--- 关键点
    if(pi->nread == pi->nwrite)
      break; // 管道空了,跳出循环
    ch = pi->data[pi->nread++ % PIPESIZE];
    if(copyout(pr->pagetable, addr + i, &ch, 1) == -1)
      break; // 拷贝到用户空间失败,跳出循环
  }
  wakeup(&pi->nwrite);
  release(&pi->lock);
  return i; // 返回实际读取的字节数 i
}

2. 初步观察

  • 循环 n 次,每次读取一个字节。
  • 如果管道为空 (pi->nread == pi->nwrite)break
  • 最后返回实际读取的字节数 i

看上去完全符合标准 read 的逻辑,没有明显错误。


3. 问题根源

问题并不在 piperead 本身,而在它与 sleep / wakeup 调度机制 的交互。这是一个典型的 并发问题


4. 正常执行时序

  1. 父进程 write("ping"),唤醒子进程。
  2. 子进程从 sleep 中被唤醒,进入 for 循环读取数据。
  3. 子进程成功读到 "ping"piperead 返回 5。
  4. 子进程打印 "ping",再 write("pong")
  5. 父进程被唤醒,read("pong") 成功。

5. 异常执行时序

  1. 父进程写入 "ping" 并唤醒子进程。
  2. 子进程被唤醒,但在 for 循环刚开始时被 上下文切换,此时 i=0
  3. 父进程执行 read(pipe2[0], ...),因为管道为空而进入 sleep
  4. 子进程再次获得 CPU,继续执行 for 循环。
  5. 由于时序异常,piperead 提前返回 0
  6. 子进程的 buf 没有被写入数据,内容是未初始化的“垃圾值”。
  7. printf 输出时因为 buf[0] == '\0',表现为空字符串。

6. 虚假唤醒与竞争条件

  • xv6 的 sleep 可能会出现 虚假唤醒 (Spurious Wakeup)
  • wakeup 信号可能在 sleep 的临界点丢失。
  • 结果是:管道中明明有数据,但 piperead 提前返回 0

这就是你观察到的“子进程读到空字符串”的原因。


7. 为什么修改 read 大小能规避问题?

  • read 大小设为 刚好消息长度 (strlen) 时,时序更简单,问题不容易出现。
  • read 大小远大于实际消息时(如 sizeof(buf)),调度点更多,更容易触发竞态。
  • 可能本质上并没有修复问题,只是改变了时序,使得 bug 难以复现。

8. 最终结论

  • 根源kernel/pipe.cpipereadsleep/wakeup 调度机制存在低概率的竞争条件。
  • 表现:在极端时序下,piperead 返回 0,导致 buf 未写入数据,输出空字符串。
  • 为什么 Linux 没问题:Linux 的管道实现更健壮,避免了这种竞态。
  • 为什么 strlen 版本正常:可能只是巧合地绕开了竞态,并非是真正解决。