marp: true theme: default paginate: true _paginate: false header: '' footer: '' backgroundColor: white
第十讲 进程间通信
Inter Process Communication, IPC
第二节 支持IPC的OS
IPC OS (IOS)
向勇 陈渝 李国良 任炬
2025年秋季
提纲
以往实验目标
提高性能、简化开发、加强安全、支持数据持久保存
- Filesystem OS:支持数据持久保存
 - Process OS: 增强进程管理和资源管理
 - Address Space OS: 隔离APP访问的内存地址空间
 - multiprog & time-sharing OS: 让APP共享CPU资源
 - BatchOS: 让APP与OS隔离,加强系统安全,提高执行效率
 - LibOS: 让APP与HW隔离,简化应用访问硬件的难度和复杂性
 
实验目标
支持应用的灵活性,支持进程间交互

实验目标
支持应用的灵活性,支持进程间交互
- 扩展文件抽象:Pipe, Stdout, Stdin
 - 以文件形式进行进程间数据交换
 - 以文件形式进行串口输入输出
 - 信号实现进程间异步通知机制
 - 系统调用数量:11个 --> 17个
- 管道:2 个、用于传数据
 - 信号:4 个、用于发通知
 
 

实验要求
- 理解文件抽象
 - 理解IPC机制的设计与实现
- pipe
 - signal
 
 - 会写支持IPC的OS
 

提纲
- 实验安排
 - 代码结构
 - 管道的设计实现
 - 信号的设计实现
 
管道实现需要考虑的问题
- 管道是啥?
 - 如何访问管道?
 - 如何管理管道?
 

理解管道
管道是内核中的一块内存
- 顺序写入/读出字节流
 
管道可抽象为文件
- 进程中包含管道文件描述符
- 管道的
FileTrait的接口 - read/write
 
 - 管道的
 - 应用创建管道的系统调用
sys_pipe
 

管道示例程序 (用户态)
...// usr/src/bin/pipetest.rs static STR: &str = "Hello, world!" //字符串全局变量 pub fn main() -> i32 { let mut pipe_fd = [0usize; 2]; //包含两个元素的fd数组 pipe(&mut pipe_fd); // create pipe if fork() == 0 { // child process, read from parent close(pipe_fd[1]); // close write_end let mut buffer = [0u8; 32]; //包含32个字节的字节数组 let len_read = read(pipe_fd[0], &mut buffer) as usize; //读pipe } else { // parent process, write to child close(pipe_fd[0]); // close read end write(pipe_fd[1], STR.as_bytes()); //写pipe let mut child_exit_code: i32 = 0; wait(&mut child_exit_code); //父进程等子进程结束 } ...
管道与进程的关系
pipe是进程控制块的资源之一

信号实现需要考虑的问题
- 信号是啥?
 - 如何使用信号?
 - 如何管理信号?
 

理解信号
signal是内核通知应用的软件中断
准备阶段
- 设定
signal的整数编号值 - 建立应对某
signal编号值的例程signal_handler 
执行阶段
- 向某进程发出signal,打断进程的当前执行,转到
signal_handler执行 

信号示例程序(用户态)
...// usr/src/bin/sig_simple.rs fn func() { //signal_handler println!("user_sig_test succsess"); sigreturn(); //回到信号处理前的位置继续执行 } pub fn main() -> i32 { let mut new = SignalAction::default(); //新信号配置 let old = SignalAction::default(); //老信号配置 new.handler = func as usize; //设置新的信号处理例程 if sigaction(SIGUSR1, &new, &old) < 0 { //setup signal_handler panic!("Sigaction failed!"); } if kill(getpid() as usize, SIGUSR1) <0{ //send SIGUSR1 to itself ... } ...
信号与进程的关系
signal是进程控制块的资源之一

提纲
- 实验安排
 - 代码结构
 - 管道的设计实现
 - 信号的设计实现
 
管道:Unix 中最引人注目的发明
- 管道的概念来自贝尔实验室的Douglas McIlroy,他在1964年写的一份内部文件中,提出了把多个程序“像花园水管一样”串连并拧在一起的想法,让数据在不同程序中流动。
 - 大约在1972年下半年,Ken Thompson在听了Douglas McIlroy关于管道的唠叨后,灵机一动,迅速把管道机制实现在UNIX中。
 

信号:Unix 中容易出错的软件中断
信号从Unix的第一个版本就已存在,只是与我们今天所知道的有点不同,需要通过不同的系统调用来捕获不同类型的信号。在版本4之后,改进为通过一个系统调用来捕获所有信号。

提纲
- 实验安排
 - 代码结构
 - 管道的设计实现
 - 信号的设计实现
 
1.1 实验目标 1.2 总体思路 1.3 历史背景
1.4 实践步骤
实践步骤
git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
cd rCore-Tutorial-v3
git checkout ch7
cd os
make run
参考输出
[RustSBI output]
...
filetest_simple
fantastic_text
**************/
Rust user shell
>>
操作系统启动shell后,用户可以在shell中通过敲入应用名字来执行应用。
测例 pipetest
在这里我们运行一下本章的测例 pipetest :
>> pipetest
Read OK, child process exited!
pipetest passed!
>>
此应用的父子进程通过pipe完成字符串"Hello, world!"的传递。
测例 sig_simple
在这里我们运行一下本章的测例 sig_simple :
>> sig_simple 
signal_simple: sigaction
signal_simple: kill
user_sig_test succsess
signal_simple: Done
>>
此应用建立了针对SIGUSR1信号的信号处理例程func,然后再通过kill给自己发信号SIGUSR1,最终func会被调用。
提纲
- 实验安排
 
2. 代码结构
- 管道的设计实现
 - 信号的设计实现
 

用户代码结构
└── user
    └── src
        ├── bin
        │   ├── pipe_large_test.rs(新增:大数据量管道传输)
        │   ├── pipetest.rs(新增:父子进程管道传输)
        │   ├── run_pipe_test.rs(新增:管道测试)
        │   ├── sig_tests.rs(新增:多方位测试信号机制)
        │   ├── sig_simple.rs(新增:给自己发信号)
        │   ├── sig_simple2.rs(新增:父进程给子进程发信号)
        ├── lib.rs(新增两个系统调用:sys_close/sys_pipe/sys_sigaction/sys_kill...)
        └── syscall.rs(新增两个系统调用:sys_close/sys_pipe/sys_sigaction/sys_kill...)
内核代码结构
├── fs(新增:文件系统子模块 fs)
│   ├── mod.rs(包含已经打开且可以被进程读写的文件的抽象 File Trait)
│   ├── pipe.rs(实现了 File Trait 的第一个分支——可用来进程间通信的管道)
│   └── stdio.rs(实现了 File Trait 的第二个分支——标准输入/输出)
├── mm
│   └── page_table.rs(新增:应用地址空间的缓冲区抽象 UserBuffer 及其迭代器实现)
├── syscall
│   ├── fs.rs(修改:调整 sys_read/write 的实现,新增 sys_close/pipe)
│   ├── mod.rs(修改:调整 syscall 分发)
├── task
│   ├── action.rs(信号处理SignalAction的定义与缺省行为)
│   ├── mod.rs(信号处理相关函数)
│   ├── signal.rs(信号处理的信号值定义等)
│   └── task.rs(修改:在任务控制块中加入信号相关内容)
└── trap
├── mod.rs(进入/退出内核时的信号处理)
提纲
- 实验安排
 - 代码结构
 
3. 管道的设计实现
- 信号的设计实现
 

管道的设计实现
基于文件抽象,支持I/O重定向
- [K] 实现基于文件的标准输入/输出
 - [K] 实现基于文件的实现管道
 - [U] 支持命令行参数
 - [U] 支持 “|" 符号
 

标准输入/输出文件
- 实现基于文件的标准输入/输出
 
- FD:0 -- Stdin ; 1/2 -- Stdout
 - 实现File 接口
- read -> call(SBI_CONSOLE_GETCHAR)
 - write -> call(SBI_CONSOLE_PUTCHAR)
 
 

标准输入/输出文件初始化
- 创建TCB时初始化
fd_table 
#![allow(unused)] fn main() { TaskControlBlock::fork(...)->... { ... let task_control_block = Self { ... fd_table: vec![ // 0 -> stdin Some(Arc::new(Stdin)), // 1 -> stdout Some(Arc::new(Stdout)), // 2 -> stderr Some(Arc::new(Stdout)), ], ... }

fork实现中的标准文件创建
fork时复制fd_table
#![allow(unused)] fn main() { TaskControlBlock::new(elf_data: &[u8]) -> Self{ ... // copy fd table let mut new_fd_table = Vec::new(); for fd in parent_inner.fd_table.iter() { if let Some(file) = fd { new_fd_table.push(Some(file.clone())); } else { new_fd_table.push(None); } } }

管道文件
- 管道的系统调用
 
#![allow(unused)] fn main() { /// 功能:为当前进程打开一个管道。 /// 参数:pipe 表示应用地址空间中 /// 的一个长度为 2 的 usize 数组的 /// 起始地址,内核需要按顺序将管道读端 /// 和写端的文件描述符写入到数组中。 /// 返回值:如果出现了错误则返回 -1, /// 否则返回 0 。 /// 可能的错误原因是:传入的地址不合法。 /// syscall ID:59 pub fn sys_pipe(pipe: *mut usize) -> isize; }

管道文件
- 创建管道中的Buffer
 
#![allow(unused)] fn main() { pub struct PipeRingBuffer { arr: [u8; RING_BUFFER_SIZE], head: usize, tail: usize, status: RingBufferStatus, write_end: Option<Weak<Pipe>>, } make_pipe() -> (Arc<Pipe>, Arc<Pipe>) { let buffer = PipeRingBuffer::new(); let read_end = Pipe::read_end_with_buffer(); let write_end = Pipe::write_end_with_buffer(); ... (read_end, write_end) }

管道文件
- 实现基于文件的输入/输出
 
- 实现File 接口
 
#![allow(unused)] fn main() { fn read(&self, buf: UserBuffer) -> usize { *byte_ref = ring_buffer.read_byte(); } fn write(&self, buf: UserBuffer) -> usize { ring_buffer.write_byte( *byte_ref ); } }

exec系统调用的命令行参数
- sys_exec 的系统调用接口需要发生变化
 
#![allow(unused)] fn main() { // 增加了args参数 pub fn sys_exec(path: &str, args: &[*const u8]) -> isize; }
- shell程序的命令行参数分割
 
#![allow(unused)] fn main() { // 从一行字符串中获取参数 let args: Vec<_> = line.as_str().split(' ').collect(); // 用应用名和参数地址来执行sys_exec系统调用 exec(args_copy[0].as_str(), args_addr.as_slice()) }

exec系统调用的命令行参数
- 将获取到的参数字符串压入到用户栈上
 
#![allow(unused)] fn main() { impl TaskControlBlock { pub fn exec(&self, elf_data: &[u8], args: Vec<String>) { ... // push arguments on user stack } }
- Trap 上下文中的 a0/a1 寄存器,让 a0 表示命令行参数的个数,而 a1 则表示图中 argv_base 即蓝色区域的起始地址。
 

exec系统调用的命令行参数
#![allow(unused)] fn main() { pub extern "C" fn _start(argc: usize, argv: usize) -> ! { //获取应用的命令行个数 argc, 获取应用的命令行参数到v中 //执行应用的main函数 exit(main(argc, v.as_slice())); } }

重定向
- 复制文件描述符系统调用
 
#![allow(unused)] fn main() { /// 功能:将进程中一个已经打开的文件复制 /// 一份并分配到一个新的文件描述符中。 /// 参数:fd 表示进程中一个已经打开的文件的文件描述符。 /// 返回值:如果出现了错误则返回 -1,否则能够访问已打 /// 开文件的新文件描述符。 /// 可能的错误原因是:传入的 fd 并不对应一个合法的已打 /// 开文件。 /// syscall ID:24 pub fn sys_dup(fd: usize) -> isize; }
重定向
- 复制文件描述符系统调用
 
#![allow(unused)] fn main() { pub fn sys_dup(fd: usize) -> isize { ... let new_fd = inner.alloc_fd(); inner.fd_table[new_fd] = inner.fd_table[fd]; newfd } }
shell重定向 "$ A | B"
#![allow(unused)] fn main() { // user/src/bin/user_shell.rs { let pid = fork(); if pid == 0 { let input_fd = open(input, ...); //输入重定向 -- B 子进程 close(0); //关闭文件描述符0 dup(input_fd); //文件描述符0与文件描述符input_fd指向同一文件 close(input_fd); //关闭文件描述符input_fd //或者 let output_fd = open(output, ...);//输出重定向 -- A子进程 close(1); //关闭文件描述符1 dup(output_fd);//文件描述符1与文件描述符output_fd指向同一文件 close(output_fd);//关闭文件描述符output_fd //I/O重定向后执行新程序 exec(args_copy[0].as_str(), args_addr.as_slice()); }... }
提纲
- 实验安排
 - 代码结构
 - 管道的设计实现
 
4. 信号的设计实现
- signal的系统调用
 - signal核心数据结构
 - 建立signal_handler
 - 支持kill系统调用
 

与信号处理相关的系统调用
- sigaction: 设置信号处理例程
 - sigprocmask: 设置要阻止的信号
 - kill: 将某信号发送给某进程
 - sigreturn: 清除堆栈帧,从信号处理例程返回
 

与信号处理相关的系统调用
#![allow(unused)] fn main() { // 设置信号处理例程 // signum:指定信号 // action:新的信号处理配置 // old_action:老的的信号处理配置 sys_sigaction(signum: i32, action: *const SignalAction, old_action: *const SignalAction) -> isize pub struct SignalAction { // 信号处理例程的地址 pub handler: usize, // 信号掩码 pub mask: SignalFlags } }

与信号处理相关的系统调用
#![allow(unused)] fn main() { // 设置要阻止的信号 // mask:信号掩码 sys_sigprocmask(mask: u32) -> isize }
#![allow(unused)] fn main() { // 清除堆栈帧,从信号处理例程返回 sys_sigreturn() -> isize }
#![allow(unused)] fn main() { // 将某信号发送给某进程 // pid:进程pid // signal:信号的整数码 sys_kill(pid: usize, signal: i32) -> isize }

信号的核心数据结构
进程控制块中的信号核心数据结构
#![allow(unused)] fn main() { pub struct TaskControlBlockInner { ... pub signals: SignalFlags, // 要响应的信号 pub signal_mask: SignalFlags, // 要屏蔽的信号 pub handling_sig: isize, // 正在处理的信号 pub signal_actions: SignalActions, // 信号处理例程表 pub killed: bool, // 任务是否已经被杀死了 pub frozen: bool, // 任务是否已经被暂停了 pub trap_ctx_backup: Option<TrapContext> //被打断的trap上下文 } }
建立signal_handler
#![allow(unused)] fn main() { fn sys_sigaction(signum: i32, action: *const SignalAction, old_action: *mut SignalAction) -> isize { //保存老的signal_handler地址到old_action中 let old_kernel_action = inner.signal_actions.table[signum as usize]; *translated_refmut(token, old_action) = old_kernel_action; //设置新的signal_handler地址到TCB的signal_actions中 let ref_action = translated_ref(token, action); inner.signal_actions.table[signum as usize] = *ref_action; }
对于需要修改的信号编号signum:
- 保存老的signal_handler地址到
old_action - 设置
action为新的signal_handler地址 
通过kill发出信号
#![allow(unused)] fn main() { fn sys_kill(pid: usize, signum: i32) -> isize { let Some(task) = pid2task(pid); // insert the signal if legal let mut task_ref = task.inner_exclusive_access(); task_ref.signals.insert(flag); ... }
对进程号为pid的进程发送值为signum的信号:
- 根据
pid找到TCB - 在TCB中的signals插入
signum信号值 
通过kill发出和处理信号的过程
当pid进程进入内核后,直到从内核返回用户态前的执行过程:
执行APP --> __alltraps 
         --> trap_handler 
            --> handle_signals 
                --> check_pending_signals 
                    --> call_kernel_signal_handler
                    --> call_user_signal_handler
                       -->  // backup trap Context
                            // modify trap Context
                            trap_ctx.sepc = handler; //设置回到中断处理例程的入口
                            trap_ctx.x[10] = sig;   //把信号值放到Reg[10]
            --> trap_return //找到并跳转到位于跳板页的`__restore`汇编函数
       -->  __restore //恢复被修改过的trap Context,执行sret
执行APP的signal_handler函数
APP恢复正常执行
当进程号为pid的进程执行完signal_handler函数主体后,会发出sys_sigreturn系统调用:
#![allow(unused)] fn main() { fn sys_sigreturn() -> isize { ... // 恢复之前备份的trap上下文 let trap_ctx = inner.get_trap_cx(); *trap_ctx = inner.trap_ctx_backup.unwrap(); ... 执行APP --> __alltraps --> trap_handler --> 处理 sys_sigreturn系统调用 --> trap_return //找到并跳转到位于跳板页的`__restore`汇编函数 --> __restore //恢复被修改过的trap Context,执行sret 执行APP被打断的地方 }
屏蔽信号
#![allow(unused)] fn main() { fn sys_sigprocmask(mask: u32) -> isize { ... inner.signal_mask = flag; old_mask.bits() as isize ... }
把要屏蔽的信号直接记录到TCB的signal_mask数据中
小结
- 管道的概念与实现
 - 信号的概念与实现
 - 能写迅猛龙操作系统
 
