20231326《密码系统设计》十周预习报告

20231326《密码系统设计》十周预习报告

目录

学习内容

《Head First C 嗨翻 C 语言》第 10 章

1. 数据流与重定向

  • 文件描述符(File Descriptor):进程用文件描述符表示数据流,它是一个数字,代表一条数据流 。
  • 描述符表(Descriptor Table) :进程内部用来记录文件描述符及其对应数据流的表格 。
    • 标准数据流 :描述符表的前三项是固定的 :
      • 0 :标准输入(stdin),默认连接键盘 。
      • 1 :标准输出(stdout),默认连接屏幕 。
      • 2 :标准错误(stderr),默认连接屏幕 。
  • 重定向(Redirection):重定向通过修改描述符表中固定编号(如1号标准输出)所对应的数据流来实现 。
  • 重定向相关的函数
    • fileno(FILE *stream) :返回文件指针 FILE* 对应的文件描述符编号 。
    • dup2(oldfd, newfd) :复制数据流,让 newfd 指向与 oldfd 相同的数据流。这是在代码中实现重定向的关键函数,例如 dup2(fileno(f), 1) 可以将标准输出重定向到文件 f

2. 错误处理

  • exit(int status) 系统调用 :用于快速终止程序,并设置退出状态(status)。它可以取代复杂的错误处理中的 return 语句,程序一旦调用 exit() 就会立刻终止,不需要返回主函数 。使用时需要包含 <stdlib.h> 头文件 。

3. 进程同步与控制

  • waitpid(pid_t pid, int *pid_status, int options) 函数 :用于让父进程等待指定的子进程结束以后才返回 。这可以解决子进程未完成任务(如文件写入)而父进程已退出导致数据丢失的问题 。
    • pid_status 用来保存进程的退出信息,必须是指针 。
    • options 通常设为 0,表示等待进程结束 。
  • WEXITSTATUS(pid_status) :用于从 pid_status 中提取子进程的退出状态(前8位),因为 pid_status 中包含了其他信息 。使用该函数需要包含 <sys/wait.h> 头文件 。

4. 管道(Pipes)

  • 管道的作用:用于连接两个进程,将一个进程的输出连接到另一个进程的输入,从而实现父子进程间的实时数据通信 。
  • pipe(int fd[2]) 函数 :创建一个管道,并返回两个文件描述符 :
    • fd[0]:管道的读取端
    • fd[1]:管道的写入端
  • 父子进程连接管道
    • 子进程 :关闭读取端 fd[0],将标准输出(1号描述符)重定向到写入端 fd[1]
    • 父进程 :关闭写入端 fd[1],将标准输入(0号描述符)重定向到读取端 fd[0]
  • 性质:管道是单向通信的,通常在存储器中实现 。

5. 信号(Signals)

  • 信号的概念:操作系统发送给进程的通知,用于中断、终止进程或进行定时等(例如用户按下 Ctrl-C) 。
  • 发送信号
    • kill 命令 :默认发送 SIGTERM 信号,可以用来发送其他信号如 SIGINTSIGKILL 信号不能被捕捉或忽略 。
    • alarm(seconds) 函数 :设置一个定时器,在指定时间后发送 SIGALRM 信号(闹钟信号) 。
    • raise(signal) 函数:让进程向自己发送信号 。
  • 捕捉信号(Signal Handling)
    • 使用 sigaction 机制注册**信号处理器(handler)**函数来处理接收到的信号 。
    • 信号处理器函数必须接收一个 int 参数(信号编号) 。
  • 修改信号行为
    • SIG_DFL:将信号处理方式还原为默认行为 。
    • SIG_IGN:让进程忽略某个信号 。

AI 对学习内容的总结

1. 数据流与重定向

  • 核心思想:每个进程内部通过文件描述符(数字)来管理数据流,前3个固定用于标准输入、输出和错误。

  • 重定向就是改变这些文件描述符指向的对象,例如输出到文件而不是屏幕。

  • 关键函数补充

    • dup(oldfd):返回一个新的文件描述符,也指向同一数据流,但不覆盖已有描述符。
    • dup2(oldfd, newfd):可以覆盖 newfd,直接实现标准输出/输入的重定向。
  • 理解点:重定向实际上是修改了内核的文件描述符表,让操作系统认为"输出到屏幕"的数据流现在去向文件或管道。


2. 错误处理

  • exit(status)可以在任意位置快速退出程序,而不必依赖返回值逐层返回。
  • 补充 :还有 _exit(status),它会直接退出进程,不调用缓冲区刷新和 atexit 注册函数,通常在子进程中用来避免重复输出。

3. 进程同步与控制

  • 父进程使用 waitpid() 等待子进程完成,避免子进程写数据未完成父进程就退出导致数据丢失。

  • 补充点

    • WIFEXITED(status):检查子进程是否正常退出。
    • WIFSIGNALED(status):检查子进程是否被信号终止。
    • 可以通过这些宏组合实现对不同退出情况的精确判断。

4. 管道(Pipes)

  • 本质:管道是内存中创建的单向缓冲区,用于进程间通信(IPC)。

  • 父子进程各自关闭不用的端,重定向标准输入输出到管道,实现数据流传递。

  • 补充

    • 管道是半双工的(单向),如果需要双向通信,需要两个管道或使用 socketpair()
    • 管道缓冲区有大小限制(通常4KB~64KB),超出会阻塞写操作。

5. 信号(Signals)

  • 信号是操作系统用来异步通知进程的机制,可以中断、终止或触发处理。

  • 补充理解

    • SIGKILLSIGSTOP 无法被捕捉或忽略,其他信号可以通过 sigaction 改变默认行为。
    • 信号处理函数最好不要做复杂操作,只做标记或简单处理,复杂逻辑容易导致竞态问题。
    • sigprocmask() 可以阻塞某些信号,防止在关键区域被中断。
    • 多个信号可能"合并",有些实时信号可以排队,普通信号可能丢失。

学习思维导图

mindmap root 第10章 学习内容 数据流与重定向 文件描述符 定义 数字表示进程数据流 描述符表 标准输入 stdin 0 标准输出 stdout 1 标准错误 stderr 2 重定向 原理 修改描述符表指向 相关函数 fileno FILE stream dup2 oldfd newfd 错误处理 exit int status 快速终止程序 设置退出状态 进程同步与控制 waitpid pid pid_status options 父进程等待子进程结束 WEXITSTATUS status 获取子进程退出状态 管道 Pipes 作用 进程间通信 pipe fd 读取端 fd0 写入端 fd1 父子进程连接 子进程 关闭fd0 dup2 fd1 1 父进程 关闭fd1 dup2 fd0 0 特性 单向通信 信号 Signals 概念 操作系统发送通知 发送信号 kill命令 SIGTERM SIGINT alarm seconds SIGALRM raise signal 捕捉信号 sigaction 注册处理器 修改行为 SIG_DFL 默认 SIG_IGN 忽略

基于 AI 的学习

原对话链接:https://chatgpt.com/share/691b01b4-1b64-800b-992e-192d846b72fe


ChatGPT: 哥们,我们先从文件描述符开始:

如果你有一个文件 output.txt,你想把程序的标准输出(stdout)重定向到这个文件,你会用哪些函数或方法来实现?请描述思路,不用写完整代码。

Gisli: fopen()打开/创建并打开文件;fprint()重定向输出,dup2()重定向标准输出到文件,最后关闭文件


ChatGPT: 不错,哥们,你抓住了关键点。接下来想问你:

当你用 dup2(fileno(f), 1) 把标准输出重定向到文件后,如果程序里还有 printf() 语句,它们输出的内容会去哪儿?为什么?

Gisli: 会输入到文件f中。因为printf是标准库函数,它的输出包含在stdout内


ChatGPT: 完全正确,哥们!printf() 默认写入 stdout,而 dup2 已经把 stdout 指向了文件,所以输出自然就到了文件里。

那我问你下一个:

假设父进程创建了一个子进程去写文件,父进程立即退出,会出现什么问题?用我们学的函数,怎么解决?

Gisli: 子进程找不到父而变成僵尸进程;父必须在创建子进程后waitpid


ChatGPT: 对头,哥们,不过稍微补充一下:

如果父进程提前退出而没有调用 waitpid,子进程在结束后会暂时成为僵尸进程 ,系统会保留它的退出信息;父进程用 waitpid(pid, &status, 0) 就可以回收这个子进程,并通过 WEXITSTATUS(status) 获取它的退出码,避免数据丢失或僵尸进程积累。

那我问你下一个:

管道(pipe)是单向的,如果父子进程都想互相通信,单纯用一个 pipe(fd) 能实现吗?为什么?

Gisli: 不能。pipe的两个文件描述符只能是从一端将数据传输到另一端;使用双管道


ChatGPT: 完全对,哥们!单个管道只能单向传输,如果父子进程都要互发数据,就必须用两个管道,一个父→子,一个子→父。

继续问你:

当一个进程注册了一个信号处理器来捕捉 SIGINT(Ctrl-C)信号,如果用户按下 Ctrl-C,会发生什么?默认行为和处理器自定义行为有什么区别?

Gisli: 进程暂停并跳转到信号处理器的代码


ChatGPT: 没错,哥们。更精确地说:

按下 Ctrl-C 时,操作系统给进程发送 SIGINT 信号,如果你注册了信号处理器,进程会暂停当前执行流,跳转到处理器函数执行自定义逻辑。默认行为下,SIGINT 会直接终止进程;而自定义处理器可以选择忽略信号、记录信息、清理资源等,然后继续执行或退出。

那我问你一个管道操作的细节:

如果父进程在读取管道前忘记关闭写入端,会发生什么?

Gisli: 在读取消息后无休止阻塞


ChatGPT: 对头,哥们。因为管道是阻塞式的,父进程读取时,如果写入端还没关闭,内核认为可能还有数据会写入,所以 read() 会一直等下去,导致阻塞。关闭写端告诉内核"没有更多数据了",read() 才能正常返回 EOF。

接下来问你一个关于 exit() 的:

调用 exit(status) 和直接 return 回到 main() 有什么区别?什么时候更适合用 exit()

Gisli: exit在无论何时都可以立即终止整个程序,而return只有main函数才结束程序


ChatGPT: 完全正确,哥们。补充一点:exit() 还会调用已注册的 atexit() 函数 ,执行缓冲区刷新和清理工作,所以在遇到错误或需要提前退出时用 exit() 更稳妥,而 return 只是结束当前函数的执行流,通常只用于 main() 结束程序。

下一个问题:

如果你想让子进程在完成任务后通知父进程自己已经结束,除了 waitpid(),还可以用什么机制?

Gisli: 子进程在关闭前使用管道对父进程发送特定消息,表示子结束


ChatGPT: 没错,哥们!用管道可以实现子进程主动通知父进程完成任务,这比单纯依赖 waitpid 更灵活,因为父进程可以实时获取子进程的状态或数据,而不仅仅是等待退出。

接下来问你一个关于信号的:

SIGKILL 信号和 SIGTERM 信号有什么本质区别?为什么 SIGKILL 无法被捕捉或忽略?

Gisli: sigterm允许执行清理操作,而sigkill直接告诉该进程终止,资源可再分配。


ChatGPT: 对头,哥们。再补充一下:

SIGTERM 是一种可捕捉、可忽略 的终止请求,进程收到后可以执行清理、保存数据、关闭文件等操作;而 SIGKILL 是操作系统强制终止信号,内核直接回收进程资源,进程完全没有机会响应或处理它,所以无法被捕捉或忽略。

接下来的问题:

如果一个进程设置了 alarm(5) 并注册了 SIGALRM 的处理器,5 秒内进程在做耗时计算,闹钟到了会发生什么?

Gisli: 终止计算,发送sigalrm信号,并跳转到信号处理函数


ChatGPT: 对头,哥们,不过稍微精确一点:

alarm(5) 到时,内核会向进程发送 SIGALRM 信号。如果进程正在执行耗时计算,计算会被中断 (即暂停当前执行流),然后跳转到你注册的信号处理函数执行自定义逻辑。处理器执行完毕后,进程通常会继续原来的计算,除非处理器里显式退出进程。

下一个问题:

dup2(oldfd, newfd) 和直接关闭 newfd 再打开文件有什么本质区别?为什么前者更适合做重定向?

Gisli: (未回答)总结以上我的回答,对我的回答内容的准确性做出评判和学习补充建议

ChatGPT: 哥们,我来帮你总结一下你这一轮回答的情况,并给出一些学习建议:


一、回答准确性总结

  1. 文件描述符与重定向 :回答准确,理解 printf() 输出会跟随 stdout 改变。
  2. 进程同步与 waitpid() :基本正确,可进一步补充 WEXITSTATUS() 获取退出码。
  3. 管道通信:理解单向、双向管道及阻塞行为,可结合 EOF 和非阻塞模式深入。
  4. 信号处理:理解信号中断执行流和默认行为,自定义处理器理解正确。
  5. exit() vs return :理解到位,可补充 atexit() 清理机制。
  6. 子进程通知父进程:管道方案可行,可结合信号提高灵活性。
  7. dup2() 与直接关闭再打开的区别:尚未回答,但理解重定向流程已打基础。

二、学习补充建议

  1. 加深系统调用与库函数关系理解。
  2. 多练习管道和信号交互场景。
  3. 关注边缘情况,如阻塞、僵尸进程、信号嵌套。
  4. 理解 dup2() 的优势:安全高效,直接重定向。

总体来看,对第十章概念掌握不错,理解流程清晰。结合实验可加深对 Unix/Linux 系统编程的理解。

学习实践过程遇到的问题与解决方式

问题一:管道的缓冲与死锁风险

管道(Pipe)在内核中通常有一个固定的缓冲区大小(例如 64KB)。如果在父进程和子进程使用管道进行通信时,其中一个进程(比如子进程)写入了大量数据,但另一个进程(父进程)没有及时读取,会发生什么?如果写入的数据量超过了缓冲区大小,程序会不会阻塞(死锁)?如果是,这种阻塞是发生在写入端还是读取端?


  1. 管道缓冲区限制

    • 在 Linux 中,管道通常有一个固定大小缓冲区(如 64KB)。
    • 当子进程写入数据时,如果缓冲区还有空余空间,写操作会立即成功。
  2. 缓冲区满时

    • 如果写入的数据量超过缓冲区剩余空间,写操作会阻塞,直到有空间可用(也就是父进程开始读取数据,释放缓冲区)。
    • 阻塞发生在写入端,因为内核会等待缓冲区腾出空间。
    • 读取端通常不会阻塞,除非管道为空且进程尝试读取数据(会等待写入端写入)。
  3. 死锁风险

    • 如果父进程完全不读取,而子进程持续写入大量数据,就会导致子进程阻塞,甚至程序陷入死锁(尤其是双向管道且两个进程都在等待对方写入/读取时)。
    • 所以在设计进程通信时,必须保证及时读取或采用非阻塞/异步 I/O,避免阻塞累积。

问题二:dup2 的原子性与线程安全

我们知道 dup2(oldfd, newfd) 是实现重定向的关键。假设一个进程是多线程的,并且在 A 线程中调用 printf() 正在向标准输出(描述符 1)写入数据,而 B 线程同时调用 dup2(new_fd, 1) 来重定向标准输出。请思考:

  1. dup2 操作本身是原子性的吗?
  2. 在上述场景中,printf() 输出的数据流向是否会产生竞争条件或不可预测的结果(例如,部分数据写入旧目标,部分数据写入新目标)?

  1. dup2 原子性

    • dup2(oldfd, newfd) 本身是原子操作:内核会立即让 newfd 指向 oldfd 对应的文件表项。
    • 但是,它只保证文件描述符表的修改是原子性的,不保证应用层多线程的 printf 与 dup2 调用之间的数据完整性。
  2. 多线程同时操作标准输出

    • 如果 A 线程在调用 printf() 写数据,而 B 线程同时执行 dup2(new_fd, 1),可能会产生以下情况:

      • A 线程正在写的数据可能被写入旧文件描述符对应的文件/终端。
      • 随后 B 线程完成 dup2,之后的 printf 输出会写入新目标。
    • 结果 :数据流向可能混杂(部分写到旧输出,部分写到新输出),形成竞争条件

  3. 解决方法

    • 在重定向前,最好暂停其他线程的输出操作,或者在单线程环境下执行重定向。
    • 对标准输出使用**线程锁(mutex)**保护 printf 调用和重定向操作,也可以避免数据错乱。

本文链接

参考资料