CSCI3150 T03 系统调用与管道
课程:CSCI3150 Introduction to Operating Systems · 2025T2 本讲:T03 系统调用与管道 · 原件 T03-作业1系统调用-2025T2.pptx(本地课件) · PDF页码副本(本地课件) p.1–13 定位:把进程创建、映像替换、描述符复制与管道EOF组合起来。 前后:前接 T02;对应 L02、L04。
逐页伴读
p.1–3:历史Shell练习的接口清单
p.1 Assignment 1: System Calls
CSCI3150 Introduction to Operating Systems, Spring 2026
p.2 Assignment 1: Implementation of A Simple Shell
Understanding how to interact with an operating system, e.g. Linux
Implement a basic shell that supports
Built-in commands
External commands
Pipe
Understanding the design choices and rationales of a shell
Deadline
Jan 26, 2026, 11:59 p.m.
p.3 All examples used in this tutorial:
https://github.com/henryhxu/CSCI3150/tree/2026-Spring/tutorial/T03
封面2026年1月22日;p.2提醒旧作业1月26日截止。p.3承接T02的行为观察,本次转入fork/exec、dup与pipe。这里不改变历史作业源码,只解释接口和例子。
p.4–6:fork与exec分开,才保住Shell管理者
课件程序与接口(p.4,原页片段)
Fork & wait
Function prototype
pid_t fork(void);
pid_t wait(int *wstatus);
fork() is used to create child processes
wait() is used to let the parent wait for one of its child to terminate
The child process created with fork() is an exact duplicate of the parent process except for several points(process id, memory lock, …)
课件程序与接口(p.4,原页片段)
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
pid_t pid = 0;
int status;
int wait_return;
for(int i=0; i<10; i++){
/* create 10 child processes in parent process */
if(pid > 0){
pid = fork();
if(pid < 0){
printf("fork() failed\n");
exit(-1);
}
}
}
if(pid == 0){
printf("Hello from child %d\n", getpid());
exit(0);
}
/* wait for all child to terminate by checking the return value of wait */
while((wait_return = wait(&status)) > 0){;}
printf("All child terminated\n");
return 0;
}
课件程序与接口(p.5,原页片段)
The exec() system call family:
int execl(const char *pathname, const char *arg, ... /*, (char *) NULL */);
int execlp(const char *file, const char *arg, ... /*, (char *) NULL */);
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
课件程序与接口(p.5,原页片段)
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
char *file_name = “./my_env";
char *argv[32] = {"my_env", NULL};
char *envp[2] = { "ABC=1", NULL};
/* execute the command */
// execl("/bin/my_env", argv[0], NULL);
// execlp(file_name, argv[0], NULL);
// execle("/bin/my_env", argv[0], NULL, envp);
// execv("/bin/my_env", argv);
// execvp(file_name, argv);
execvpe(file_name, argv, envp);
printf("We just arrive at a line of code after exec()\n");
return 0;
}
p.6 Combining fork() and exec()?
Child process will have its own image, no longer a duplicate of its parent
课件程序与接口(p.6,原页片段)
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
pid_t pid;
pid = fork();
if(pid == -1){
printf("Fork error\n");
exit(-1);
}
else if(pid > 0){
/* parent */
printf("parent is doing something\n");
sleep(5);
printf("parent finished its task");
}
else{
/* child */
execlp("ls","ls","-a",NULL);
}
return 0;
}
p.4原例意图父进程循环创建10子进程,但截图实际先 pid_t pid=0,循环内又仅在 pid>0时fork,因此原样运行一次fork也不会发生,原进程最后反而走pid==0分支。要实现注释所说的10个子进程,必须先让父进程满足进入条件或重构分支;不能把注释当执行事实。假设意图修正且均成功,则新建10、连父共11,而不是每轮所有进程翻倍的1024。
p.5 exec名字里的l/v表示参数列表/数组,p表示按PATH寻找,e表示显式环境数组。例子envp仅给ABC=1;成功exec替换当前程序,后面的printf不执行,只有失败才返回。p.6先fork、子exec ls,父可继续自身逻辑;sleep(5)是延时,不是可靠等待子进程结束,应使用wait类接口管理结束。
p.4 的原程序与旁边的文字目标不一致(已视觉核对):页上把 pid 初始化为 0,循环里却只在 pid > 0 时调用 fork。第一次测试是 0 > 0,结果为假;循环体未改变 pid,第二到第十次仍为假。因此按所展示代码执行,fork 一次也不会发生;循环结束后 pid == 0 成立,原进程自己打印 “Hello from child” 并 exit。它既不会生成注释声称的十个孩子,也不会走到最后的“All child terminated”。
要表达该页想教的“只有父进程继续创建”的逻辑,必须先让原父进程满足创建分支,并让成功 fork 后获得 0 的孩子退出创建路径。以父进程初始标记为正数的教学修正来推演:每轮父进程创建一个孩子,父分支得到正的孩子 PID,因而能进行下一轮;孩子得到 0,不再 fork;十轮后总计一个父进程和十个孩子。这里把课件原样与意图修正分开,未改动原课件,也不把有缺陷的原例当成已运行成功的程序。
p.7–9:复制的是描述符引用,管道传的是字节
课件程序与接口(p.7,原页片段)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include <fcntl.h>
int main(){
char *file_path = "./test0.txt";
int fd = open(file_path, O_RDWR);
/* redirect standard output to the file */
dup2(fd,STDOUT_FILENO);
printf("------dup test ends------\n");
close(fd);
return 0;
}
课件程序与接口(p.7,原页片段)
dup
Function prototype
int dup(int oldfd);
int dup2(int oldfd, int newfd);
Allocates a new file descriptor that refers to the same open file description as the descriptor oldfd. For dup2, it specifically uses newfd instead of allocating a new file descriptor.
On success, these system calls return the new file descriptor. On error, -1 is returned.
Question: How to restore standard output after the dup2()?
课件程序与接口(p.8,原页片段)
Pipe
System call for pipe creation
int pipe(int pipefd[2])
Pipe characteristics
Unidirectional
“Half-duplex”
Byte-stream
How does the reader know EOF?
IPC between related processes(fork())
Notes for correctly using pipe
Always close the un-used read/write end of a pipe
课件程序与接口(p.9,原页片段)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
int pipe_fd[2];
int bytes_read;
int total_bytes_read = 0;
char readbuf[1024];
/* create a pipe */
int ret = pipe(pipe_fd);
if(ret < 0){
printf("create pipe error\n");
return 0;
}
/* write something into the pipe */
write(pipe_fd[1], "------\npipe test1\n------\n", 25);
write(pipe_fd[1], "pipe test2\n------\n", 18);
/* close the write end, EOF will be added into the pipe */
close(pipe_fd[1]);
/* read from the pipe into a buffer until there is no content in the pipe */
while ((bytes_read = read(pipe_fd[0], &readbuf[total_bytes_read], 1024-1-total_bytes_read)) > 0){
total_bytes_read += bytes_read;
}
/* print the content in the buffer */
printf("total read %d\n",total_bytes_read);
readbuf[total_bytes_read]='\0';
printf("%s\n", readbuf);
/* close read end of pipe */
close(pipe_fd[0]);
}
课件程序与接口(p.9,原页片段)
Basic usage
Function prototype
int pipe(int pipefd[2]);
Pipe() creates a pipe, a unidirectional data channel that can be used for inter-process communication.
pipefd[0]: read end of the pipe
pipefd[1]: write end of the pipe
On success, zero is returned. On error, -1 is returned
dup返回新的可用fd,dup2把指定newfd指向同一open file description。原例 dup2(fd,STDOUT_FILENO)后printf仍写标准输出,只是目的地变文件。恢复前应先保存原stdout,例如 saved=dup(1),重定向输出完成并适当fflush后 dup2(saved,1)再关闭saved;不是把原文件fd关掉就自动恢复终端。
图p.8先pipe再fork,父子都继承读写端;选择父写子读时,父关读端、子关写端。p.9先写25字节的第一串及18字节的第二串,再关写端,若两次write完整成功,共43字节;读循环累加实际返回值,末尾自行补NUL。EOF不是被写入管道的特殊字符,而是所有写端引用关闭且缓冲已读空时read返回0。
回答 p.7 的原问题 “How to restore standard output after the dup2()?”:重定向前用 saved = dup(STDOUT_FILENO) 保存旧标准输出的一份引用;确认成功后再 dup2(fd, STDOUT_FILENO)。需要恢复时先处理尚未输出的 stdio 缓冲,再 dup2(saved, STDOUT_FILENO),确认恢复成功后 close(saved)。不能先关闭唯一的旧引用再试图从整数 1 找回它,因为 1 已经指向新文件。这里 saved 是另一个描述符,保存的是对旧打开对象的引用,不是把屏幕文字复制下来。
p.9 两次 write 的字符串按原字节计分别长 25 与 18,共 43B;都不包括字符串字面量末尾的零字节。read 可能分多次收齐,所以代码用 total_bytes_read 累加,并在数据末尾自行加 \0,才可用 %s 打印。原注释 “EOF will be added into the pipe” 容易误导:关闭最后一个写端没有塞入一个 EOF 字符;它改变的是内核端点引用状态。读完 43B 后、所有写端均关闭时,下一次 read 才返回 0。
p.10–11:遗留写端会让读取等不到EOF
p.10 IPC
Pipe is one of the things that will be inherited by child processes created with fork()
Inter process communication(IPC) can be achieved with pipe
pipefd[0]: read end of the pipe
pipefd[1]: write end of the pipe
On success, zero is returned. On error, -1 is returned
课件程序与接口(p.10,原页片段)
pid = fork();
if(pid == -1){
perror("fork");
assert(false);
}
else if(pid == 0){
/* close un-unsed write end of the pipe */
close(pipe_fd[1]);
/* read from parent process into the buffer*/
while ((bytes_read = read(pipe_fd[0], &readbuf[total_bytes_read], 1024-1-total_bytes_read)) > 0){
total_bytes_read += bytes_read;
}
/* print the content in the buffer */
readbuf[total_bytes_read]='\0';
printf("[Message from parent] %s\n", readbuf);
exit(0);
}
else{
/* close un-used read end of the pipe */
close(pipe_fd[0]);
// sleep(5);
/* write something into the pipe */
write(pipe_fd[1], "Hello child process", 20);
/* finished writing, close write end so child sees EOF */
close(pipe_fd[1]);
/* wait for the child process to terminate */
while((wait_return = wait(&status)) > 0);
}
return 0;
}
课件程序与接口(p.10,原页片段)
#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h> /* perror */
#include <stdlib.h>
#include <sys/wait.h> /* wait, sleep */
#include <unistd.h> /* fork, write */
int main() {
pid_t pid;
int wait_return, status;
int pipe_fd[2];
int bytes_read;
int total_bytes_read = 0;
char readbuf[1024];
/* create a pipe */
int ret = pipe(pipe_fd);
if(ret < 0){
perror("create pipe error\n");
return 0;
}
p.11 Pipe w/ read/write
example6.c
If all file descriptors referring to the write end of a pipe have been closed
Read from pipe will see end-of-file(EOF)
If a process attempts to read from an empty pipe
Read will block until data is available
example7.c
If all file descriptors referring to the read end of a pipe have been closed
Write to the pipe will generate SIGPIPE for the process
If a process attempts to write to a full pipe
Write blocks until sufficient data has been read from the pipe to allow the write to complete
课件程序与接口(p.11,原页片段)
#include <assert.h>
#include <signal.h>
#include <stdio.h> /* perror */
#include <stdlib.h>
#include <sys/wait.h> /* wait, sleep */
#include <unistd.h> /* fork, write */
void signal_handler(int sig) {
char s1[] = "SIGPIPE captured\n";
char s2[] = "Unknown Signal\n";
if (sig == SIGPIPE){
write(STDOUT_FILENO, s1, sizeof(s1));
}
else{
write(STDOUT_FILENO, s2, sizeof(s2));
}
}
int main() {
pid_t pid = getpid();
int pipe_fd[2];
/* create a pipe */
int ret = pipe(pipe_fd);
if(ret < 0){
printf("create pipe error\n");
return 0;
}
/* create a signal handler to capture signal pipe */
signal(SIGPIPE, signal_handler);
/* close all read end of the pipe */
close(pipe_fd[0]);
/* writer something to the pipe */
write(pipe_fd[1], "test content", 12);
return 0;
}
父写子读例先建立管道再fork,子关写端后读,父关读端后写 Hello child process的20字节(含末尾NUL),关闭写端,最后wait。子若忘关自己继承的写端,即使父已关闭,系统仍认为存在写者,读空后可能继续阻塞。
空管道且仍有写端时阻塞读;所有写端关闭且数据耗尽才EOF。所有读端关闭时写会引发SIGPIPE,若信号被捕获或忽略,write仍可能以EPIPE失败。满管道的阻塞写须等读者腾空间;“返回一次必读写全部请求”不是通用保证。
补充:管道是否可读,要同时看缓冲区与所有写引用。 缓冲区非空时,读者可以取得现有字节,即使所有写者已经退出;只有这些字节读完,正长度读取才会报告 EOF。缓冲区空但仍有写引用时,阻塞式读可能继续等待,因为系统不能排除将来还有字节到来。缓冲区空且所有写引用都关闭时,才具备 EOF 条件。
因此“最后一个写者关闭”不是把管道里尚未读出的内容一并删除,“没有新输出”也不是 EOF 的同义词。诊断卡住时用两个独立问题定位:管道还有多少待读数据;系统中是否仍有任何进程保留写引用。只在消费者那一行多加一次 read,无法修复父进程遗留写端造成的永久等待。
p.12–13:组合顺序就是资源生命周期
p.12 * Redirect proper fd to proper fd, here I just use stdout/stdin as an example
p.12 How to use the above systems calls to achieve inter-process communication, where some processes may run an external binary?
图p.12依次pipe→fork→关闭不用端→把写端接stdout、读端接stdin→exec外部程序。dup之后多余原fd也应关闭,使每个端点引用数量符合预期。先等生产者结束再启动消费者,数据超过管道容量时可能让前者写满而后者尚未运行,故管道各段须能并行推进。p.13为Q&A。
用描述符账本检查一条两段管道(补充)
设管道读端3、写端4,父进程fork两个孩子,分别作为生产者与消费者(为完整Shell结构自取的编号;不同于课件父进程本身exec的简化版本)。fork后每个进程都继承3和4。生产者dup2(4,1)后保留1这份写引用,关闭3与原4;消费者dup2(3,0)后保留0这份读引用,关闭原3与4;父管理者不传输数据,关闭自己3与4,再回收两个孩子。
| 进程 | 最后仍需的管道引用 | 为什么必须关其余引用 |
|---|---|---|
| 生产者 | stdout 1→写端 | 不该保留读端来掩盖“所有读者已退出” |
| 消费者 | stdin 0→读端 | 不该保留写端来阻止EOF |
| 父Shell | 无 | 不能因管理者遗留写端使消费者一直等 |
每次dup2后看引用,而不只看整数名:close(4)不会关闭生产者的1,因为它们是两个描述符引用。若dup2失败则不能继续exec假装已接好管道;若第二次fork失败,父侧也要关已建端点并处理第一个已启动孩子,不能只回到提示符留下后台残片。
自测:消费者退出了,生产者为什么不一定立刻被系统终止?
只有生产者接下来向已无读端的管道写时才触发相关错误/信号条件;它若正做计算,未必立即观察到变化。若其他进程还遗留读端,甚至“没有读者引用”的条件还没成立。要查所有引用及实际调用,而不是只查预期消费者PID。
本讲核心考点
- [C] 按实际初值与分支数fork,区分意图与代码事实。
- [C] 解释exec成功不返回和dup2重定向恢复。
- [C] 依据所有端点引用判断EOF、阻塞和SIGPIPE。
- [C] 从管道图写出每个进程应保留和关闭的fd。
附录
本份没有需要移至附录的长推导;计算过程已在对应页正文写全。
来源与证据
-
原件:T03-作业1系统调用-2025T2.pptx(本地课件);PDF副本(本地课件) p.1–13。本文页码均为 PDF 页号。
-
考试证据:读了 课程大纲-2026T1.pdf、教学计划-2026T1.pdf、T08-期中参考-2025T2-题目-解答.pdf、T07-多级反馈队列-2025T2.pdf(p.13–15内嵌历史试题);未识别用途的 PDF:无
-
2026-10-13 内容补全:保留原页顺序与版本,补入对应页的程序、接口、字段、条件和演算;原程序片段保留课件行号、拼写与省略处,不能据此视为经过编译的完整软件。本文已有运行记录属于其标注日期,本次未重新运行课程容器或提交作业。