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

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

学习内容

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

一、 系统调用和内核 (System Calls and Kernel)

  • 系统调用 (System Call): C程序与操作系统内核沟通的机制 。它是操作系统内核中的函数,C标准库的大部分功能(例如printf())都依赖于它们来与硬件交互 。
  • 内核 (Kernel): 计算机中最重要的程序,负责管理三方面:
    • 进程 (Processes): 加载程序到内存并创建进程,确保它们获得所需资源,并监控进程状态 。
    • 存储器 (Memory): 分配和管理每个进程的存储器大小,并可能使用磁盘交换来增加虚拟存储器空间 。
    • 硬件 (Hardware): 通过设备驱动与计算机上的设备(如键盘、屏幕)进行交互 。

二、 system() 函数

  • 功能: 接收一个字符串参数,并将其作为命令执行 。
  • 用途: 是在代码中运行其他程序的捷径,尤其适用于建立快速原型,可以避免编写大量的C代码 。
  • 缺点/安全问题:
    • system()必须解释命令字符串,这容易出错 。
    • 存在命令行注入的安全风险,用户可以通过在输入文本中注入命令行代码(例如使用&&)来运行任意命令 。
    • 其他问题包括注释文本中的引号(撇号)破坏命令引号,以及PATH变量可能导致调用错误的程序 。

三、 exec() 家族函数

  • 功能: 运行其他程序来替换当前的进程 。新程序启动后会沿用原有进程的PID(进程标识符)。它通过明确指定要运行的程序来消除system()的歧义问题 。
  • 进程 (Process): 存储器中运行的程序,由一个数字(进程标识符,PID)来标识 。
  • 函数命名规则: exec 后可跟 lv,以及可选的 pe
    • l (List): 接收参数列表 。
    • v (Vector): 接收参数数组或向量 。
    • p (PATH): 根据 PATH 环境变量查找程序 。
    • e (Environment): 接收环境变量字符串数组 。
    • 参数列表函数示例: execl(), execlp(), execle() 。参数列表必须以 NULL 结束 。
    • 参数数组函数示例: execv(), execvp(), execve()
  • 环境变量 (Environment Variables):
    • 每个进程都有一组环境变量 。
    • C程序可以使用 stdlib.h 中的 getenv() 函数读取环境变量 。
    • 使用以 e 结尾的 exec() 函数(如 execle())可以传递新的环境变量数组,数组格式为 "变量名=值",并以 NULL 结束 。

四、 错误处理 (Error Handling)

  • exec() 错误判断: 如果 exec() 调用成功,当前程序会停止运行;如果失败,程序会继续运行 exec() 之后的代码 。
  • 失败黄金法则: 大多数系统调用失败时会遵循以下原则:
    • 返回 -1
    • 将全局变量 errno 设为错误码 。
  • errno: 定义在 errno.h 中的全局变量,用于保存系统调用的错误码 。
  • strerror(): 定义在 string.h 中,可以将 errno 错误码转换为可读的标准错误消息 。

五、 fork() 函数

  • 问题背景: 由于 exec() 会替换当前进程,如果在循环中直接调用 exec(),程序只会执行一次 exec() 就会终止 。
  • 功能: fork() 系统调用会克隆当前进程,创建一个新的子进程 。父进程和子进程会从 fork() 调用后的同一行代码开始执行 。
  • 返回值区分:
    • 子进程 (Child Process): fork() 返回 0
    • 父进程 (Parent Process): fork() 返回 非零值(即子进程的PID)。
    • 失败: fork() 返回 -1
  • fork() + exec() 模式:子进程 中调用 exec() 函数来替换自身运行新程序,从而允许 父进程 继续运行,实现并发或连续操作 。
  • 效率技巧: 操作系统使用“写时复制”(copy on-write)技术,父子进程最初共享数据,只有当其中一方尝试修改数据时,系统才会进行复制.
  • 平台限制: Windows 系统本身不支持 fork(),但在 Cygwin 等环境中可以通过模拟实现 。

AI对学习内容的总结

一、系统调用与内核机制

核心思想:
系统调用(System Call)是用户程序与内核之间的接口。当 C 程序需要操作硬件(例如读写文件、创建进程等)时,必须通过系统调用进入内核态执行。

要点补充:

  • 系统调用通过 CPU 的特权指令(如 syscallint 0x80)进入内核。

  • 用户态与内核态的隔离是操作系统安全与稳定的基础。

  • Linux 系统调用的典型例子:

    • 进程管理:fork()exec()
    • 文件 I/O:open()read()write()
    • 网络通信:socket()bind()connect()

二、system()exec() 的区别

特性 system() exec() 家族
执行方式 通过 shell 解释命令 直接执行目标程序
是否替换当前进程
安全性 存在命令注入风险 安全性更高
适用场景 快速执行外部命令 受控地运行外部程序

说明:

  • system() 本质上相当于执行:fork()exec("/bin/sh", "-c", command)wait()
  • 因为 system() 需要调用 shell 解释命令字符串,因此容易受输入注入攻击。
  • 在安全性要求高的程序中,应使用 fork() + exec() 替代。

三、exec() 家族函数详解

exec() 是一组函数族,核心作用是用新程序替换当前进程。
这些函数的命名体现了参数传递和环境处理方式的差异:

后缀 含义 示例
l 参数以列表形式给出 execl()
v 参数以数组形式给出 execv()
p 按 PATH 搜索可执行文件 execlp()execvp()
e 可自定义环境变量 execle()execve()

示例:

execl("/bin/ls", "ls", "-l", NULL);
execvp("ls", argv);

关键点:

  • 执行成功后不会返回;失败时返回 -1 并设置 errno
  • 使用 e 系列函数可以改变子进程的环境变量。

四、错误处理机制

通用规则:
大多数系统调用在失败时会:

  1. 返回 -1
  2. 设置全局变量 errno 以保存错误码。

配合 strerror() 使用示例:

int fd = open("file.txt", O_RDONLY);
if (fd == -1) {
    printf("Error: %s\n", strerror(errno));
}

补充理解:

  • errno 定义在 <errno.h> 中。
  • strerror() 定义在 <string.h> 中,将错误码转为可读文本。
  • 在多线程环境中,每个线程有独立的 errno

五、fork():进程创建的基础

fork() 用于创建一个新的子进程。新进程与父进程几乎完全相同,从 fork() 调用处继续执行。

返回值:

  • 父进程中:返回子进程的 PID(正数)
  • 子进程中:返回 0
  • 失败:返回 -1

典型模式:fork() + exec()

pid_t pid = fork();
if (pid == 0) {
    execlp("ls", "ls", "-l", NULL);
} else if (pid > 0) {
    wait(NULL);
}

补充:

  • Linux 使用“写时复制”(Copy-on-Write)优化,只有在数据被修改时才实际复制内存页。
  • Windows 没有 fork(),通过 CreateProcess() 创建新进程。

六、进一步延伸

  1. wait()waitpid()
    父进程通过它们等待子进程结束,防止产生僵尸进程。

  2. 进程标识函数

    • getpid():获取当前进程 PID。
    • getppid():获取父进程 PID。
  3. 进程间通信(IPC)机制

    • pipe()、共享内存、信号量、消息队列等是多进程编程的重要工具。
  4. 系统调用与库函数的关系

    • 系统调用由内核实现;
    • C 标准库函数是对系统调用的封装和扩展。
      例如:
      fopen() → 调用 open()
      fread() → 调用 read()

七、学习建议

  • 实践练习:

    • 使用 fork() 创建多个子进程;
    • 每个子进程调用 exec() 执行不同程序;
    • 父进程使用 wait() 等待全部子进程结束。
  • 对比 system()exec() 的行为差异。

  • 阅读 Linux 手册页:
    man 2 forkman 3 execman 3 system


学习思维导图

mindmap root((系统调用与进程管理)) 系统调用与内核 系统调用 C程序与内核沟通机制 内核职责 进程管理 内存管理 硬件控制 system函数 功能 执行命令字符串 优点 快速原型 缺点 注入风险 引号错误 PATH问题 exec函数族 功能 用新程序替换当前进程 变体 l 参数列表 v 参数数组 p 使用PATH e 环境变量 环境变量 getenv读取 execle_execve传递 错误处理 失败返回 -1 errno保存错误码 strerror转换为错误信息 fork函数 功能 克隆当前进程 返回值 子进程 0 父进程 子进程PID 用法 子进程exec 父进程继续 优化 写时复制 限制 Windows不原生支持

基于AI的学习

原对话链接:https://chatgpt.com/share/6911cc4b-5d54-800b-88ae-dc7ac6ff2d3d


1. 系统调用的核心作用

提问:
系统调用的核心作用是什么?为什么 C 标准库中的很多函数需要依赖系统调用?

回答:
系统调用是C程序与操作系统内核进行通信的机制,是应用程序访问计算机硬件和系统资源(如进程、内存、文件、网络等)的唯一途径。


2. 内核管理三方面

提问:
内核在计算机系统中主要负责哪三方面的管理?你能分别简单说明它们的作用吗?

回答:
处理机、存储、设备

补充说明:

  • 内核负责管理 进程 (Processes)内存 (Memory)硬件设备 (Hardware)
  • 每一项作用:
    • 进程管理:加载程序、分配资源、监控状态。
    • 内存管理:分配内存空间、使用虚拟内存。
    • 硬件管理:通过驱动控制设备。

3. “为什么不是处理机”

提问:
我这里不明白为什么不是处理机,请讲解。

回答说明:

  • 内核管理的是 进程的执行,而不是 CPU 硬件本身。
  • 内核分配 CPU 时间片给进程,CPU 执行指令,但 CPU 硬件自身不受内核直接控制。
  • 所以内核职责是 进程、内存、硬件,不是“处理机”。

4. 系统调用图解

提问:
可以画个图说明吗?

图示:

+---------------------------+
|        用户程序           |
|  (进程A、进程B……)      |
+---------------------------+
             |
             | 系统调用 / 进程请求 CPU
             v
+---------------------------+
|          内核             |
|  - 进程管理(调度、创建、终止) |
|  - 内存管理(分配、保护、虚拟内存)|
|  - 硬件管理(通过驱动控制设备) |
+---------------------------+
             |
             | 调度 CPU 执行指令
             v
+---------------------------+
|           CPU             |
|  执行指令,处理数据       |
+---------------------------+

5. system() 函数优缺点

提问:
system() 函数可以用来在 C 程序中运行其他程序。你觉得它有哪些优点和潜在的安全风险?

回答:
优点:简便
缺点:shellcode注入风险

补充说明:

  • system() 调用 shell 执行命令,易受命令行注入攻击。
  • 示例:
char cmd[256];
snprintf(cmd, sizeof cmd, "ls %s", user_input);
system(cmd); // 不安全,用户输入可包含 ; && 等
  • 安全替代:使用 fork() + exec*()posix_spawn(),直接传参数数组,不通过 shell。

6. fork() 返回值

提问:
fork() 调用后如何通过返回值区分父进程和子进程?

回答:

  • 0 → 子进程
  • 非零正数 → 父进程(子进程 PID)
  • -1 → 创建失败

7. fork() 与 exec() 的配合

提问:
为什么我们常把 fork()exec() 配合使用,而不是直接在父进程中调用 exec()

回答:

  • exec() 会替换当前进程,如果直接在父进程调用,父进程消失,后续代码不再执行。
  • fork() 创建子进程,子进程执行 exec(),父进程继续运行,实现并发或连续操作。

8. 对话总结

回答总结:

  1. 系统调用理解正确。
  2. 内核管理三方面:进程、内存、硬件,概念比“处理机”更准确。
  3. system() 简便但易受命令注入攻击,推荐使用 fork() + exec*()
  4. fork() 返回值理解正确,配合 exec() 实现子进程执行新程序而父进程继续运行。

建议:

  • 精确使用术语(进程、内存、硬件)。
  • 写实验程序观察 fork() + exec() 的执行。
  • 绘制内核与 CPU、父子进程流程图帮助记忆和理解。

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

1. Windows 与类 Unix fork() 模型设计理念差异及优势

fork() 系统调用是许多类Unix系统的核心功能,但Windows原生不支持,需要Cygwin等工具进行模拟。你认为 Windows 采用的与类 Unix 系统完全不同的进程管理方式,在设计理念上有什么核心优势(或者说,它解决了类 Unix fork() 模型可能存在的哪些问题)?

类 Unix 系统(Linux、macOS 等)

  • fork() 是核心进程创建机制:子进程是父进程的完整复制,包括内存空间、打开的文件描述符等。

  • 优点:

    • 很自然的并发模型,父子进程几乎完全相同。
    • exec() 结合,可以非常灵活地在子进程中替换程序,形成多进程服务。
  • 缺点:

    • 资源开销大:尤其是内存复制,虽然现代操作系统使用“写时复制”优化,但在大内存进程或频繁创建进程时仍有开销。
    • 复杂的父子状态管理:子进程继承大量状态,可能导致资源冲突或管理难度增加。

Windows 进程模型

  • Windows 不使用 fork(),而是通过 CreateProcess() 直接创建一个新进程,并可指定初始程序和参数。

  • 核心优势:

    1. 更轻量的进程创建

      • 不复制整个父进程内存空间,避免“写时复制”也可能带来的性能损耗。
    2. 更明确的资源边界

      • 新进程可以指定只继承需要的句柄,不会无意中继承大量父进程状态。
    3. 安全性更高

      • 避免父子进程共享过多内部状态,减少潜在的冲突和隐式依赖。
    4. 适合多语言和多环境

      • Windows 的 API 直接支持指定可执行文件和启动参数,简化跨语言或跨程序调用。
  • 总结:Windows 的设计理念是明确控制进程边界和资源继承,避免 fork() 模型可能引发的资源开销和复杂继承问题。


2. fork() 缺失对 C 程序开发或部署的影响

fork() 在Windows上的缺失是否限制了某些特定类型C程序的开发或部署?

  • 对一般 C 程序影响有限

    • 许多 C 程序只依赖标准库和线程,或者使用 system()/CreateProcess() 执行外部命令,几乎不依赖 fork()
  • 对某些类型程序有限制

    1. Unix 风格的多进程服务

      • 例如 Web 服务器或守护进程利用 fork() 快速创建子进程处理请求。
      • 在 Windows 上必须改写为线程模型或使用 CreateProcess(),增加移植工作。
    2. 某些工具和脚本

      • 使用 fork() + exec() 的工具链(如某些 shell 脚本、管道处理程序)无法直接在 Windows 上运行。
    3. 教学或实验程序

      • 在操作系统实验、并发控制练习中,无法直接用 fork() 演示父子进程的行为。
  • 解决方式

    • 使用 Cygwin 或 WSL 模拟 fork()
    • 使用 Windows 原生 线程、CreateProcess()、管道机制 重构程序。

本文链接

参考资料

posted @ 2025-11-10 19:33  GisliW  阅读(10)  评论(0)    收藏  举报