6177 字
31 分钟
NJU-OS 笔记 week3-4
2025-03-25

程序和进程#

虚拟化

  • One of the most fundamental abstractions that the OS provides to users: the process

  • 把物理计算机“抽象”成“虚拟计算机”,程序好像都独占计算机运行


5.1 程序和进程#

  • 程序是状态机的静态描述

    • 描述了所有可能的程序状态

    • 程序(动态)运行起来,就成了进程(进行中的程序)

  • 进程:程序的运行时状态随时间的演进

  • 除了程序状态,操作系统还会保存一些额外的(只读)状态

    • 进程号 pid,不在程序中,通过系统调用getpid()向操作系统获取

proc.c

  • getpid getppid getuid(user) getgid(group) geteuid(effective) getegid getpriority

  • process status from /proc/self/status

  • command line from /proc/self/cmdline

  • current working directory from /proc/self/cwd

注意代码质量,利用大语言模型code review

Prompt: 我发现随着进程的创建,进程的 pid 是递增的;而 pid 是有限的 (32-bit 整数),这是否意味着会循环导致 pid 重用?

  • 系统会维护一个 PID 位图或类似的数据结构来跟踪已使用和可用的 PID

    • 新进程创建时,内核会分配下一个可用的 PID;当 PID 达到最大值时,内核会回绕(wrap around)到最小值(通常是 300 或 1)重新分配
  • 系统不会立即重用刚刚释放的 PID,通常会有一个”冷却期”

  • 这是为了避免将新进程的 PID 分配给可能仍被其他进程引用的已终止进程,否则可能导致权限问题

    • 竞态条件:在多线程或多进程环境中,PID 重用可能导致意外行为

Testkit


5.2 进程 (状态机) 管理#

操作系统 = 状态机的管理者

  • 进程管理 = 状态机管理

一个直观的想法

  • 创建状态机:spawn(path, argv) (孵化)

  • 销毁状态机: _exit()

    • exit()已经被libc占用了

    • 这是一个合理的设计 (例子:Windows)

UNIX 的答案

  • 复制状态机: fork()

  • 复位状态机: execve()


1. 创建状态机#

pid_t fork(void);

立即复制状态机

  • 包括所有状态的完整拷贝

    • 寄存器 & 每一个字节的内存

      • 写时复制(Copy-On-Write, COW):复制父进程的虚拟内存空间,但物理内存页仅在修改时复制
    • Caveat: 进程在操作系统里也有状态: ppid, 文件, 信号, …

      • 共享文件表项、文件偏移量共享、独立关闭

      • 小心这些状态的复制行为

    • 复制失败返回 -1

      • errno 会返回错误原因 (man fork)

如何区分两个状态机?

  • 新创建进程返回 0

  • 执行 fork 的进程返回子进程的进程号——“父子关系”


进程的创建关系形成了进程树

ABCA \rightarrow B \rightarrow C,如果 BB 终止了,CC 的 ppid 是什么?

  • 错误认知:认为 CC 的ppid会变成 AA (祖父进程)

  • 实际规则:

    • 子进程终止会通知父进程

      • 通过 SIGCHLD 信号,通知父进程其子进程的状态发生了变化(例如终止、停止或恢复)

      • 父进程可以捕获这个信号 (waitpid())

    • AA 可以捕获信号并回收 BB,但不影响 CCppid

托孤机制

  • 父进程退出(正常退出或崩溃)后,内核会遍历其所有存活的子进程(成为孤儿进程)

  • 将所有子进程的父进程 PID(ppid)设置为 init 进程(PID=1,现代系统可能是 systemdlaunchd

  • 确保子进程不会因父进程消失而失去管理,避免成为僵尸进程

    • init 进程会定期调用 wait() 系统调用,回收孤儿进程的退出状态

    • 若子进程终止,init 会立即回收它,避免其长期滞留为僵尸进程


fork bomb,创建一棵进程子树

一行shell命令: :(){ :|:& };:(bash允许冒号作为标识符)

  • | 强制 Shell 为左右两侧命令各创建一个进程

  • & 将整个管道命令放到后台执行,避免阻塞 Shell

  • 1 变 2, 2 变 4, 第 k 层有 2k12^{k-1} 个进程

  • 曾经会使系统资源耗尽、彻底卡死,但现在 Linux 有 OOM(Out Of Memory) 保护,当内存严重不足时,内核的 OOM Killer 会终止高内存占用的进程

缓冲区 输出为terminal和pipe的区别

for (int i = 0; i < 2; i++) {
    fork();
    printf("Hello\n");
}
  • ./a.out 输出6行 2×2+1×22 \times 2 + 1 \times 2

  • ./a.out | cat 输出8行 2×42 \times 4

  • 终端设备(TTY):默认使用 行缓冲模式(Line-buffered),遇到换行符 \n 时立即刷新缓冲区

  • 管道/文件:默认使用 全缓冲模式(Fully-buffered),缓冲区满或程序结束时才刷新

    • fork()会复制父进程的未刷新缓冲区到子进程

2. 复位状态机#

  • 将当前进程重置成一个可执行文件描述状态机的初始状态

  • 操作系统维护的状态不变:进程号、目录、打开的文件

    • O_CLOEXEC(Close-On-Exec)

    • 若在打开文件时指定 O_CLOEXEC 标志,当进程调用 exec() 执行新程序时,该文件描述符会自动关闭

    • 防止子进程继承父进程的无关文件描述符,避免资源泄漏或安全问题

int execve(const char *pathname, char *const argv[],
           char *const envp[]);

execve 是唯一能够 “执行程序” 的系统调用

  • 因此也是一切进程 strace 的第一个系统调用

execve() 设置了进程的初始状态

  • argc & argv: 命令行参数

    • main 的参数由 execve 的 argv 参数传递

    • argc 是 argv 数组的长度

  • envp: 环境变量

    • 通过 env 命令查看当前环境变量(PATH, PWD, HOME, DISPLAY, PS1, …)

    • export: 在 Shell 中设置环境变量,子进程会通过 execve 继承

  • 程序被正确加载到内存

    • 代码、数据、PC 位于程序入口(如ELF的e_entry地址)
  • 所有 exec 系列函数(如 execl, execvp)最终通过 execve 实现程序加载

    • execl通过可变参数列表传递命令行参数
int execl(const char *pathname, const char *arg, ...
                /* (char  *) NULL */);

例子:对于可执行文件搜索路径,搜索顺序恰好是 PATH 环境变量里指定的顺序


3. 销毁状态机#

void _exit(int status);

立即摧毁状态机,允许有一个返回值,返回值可以被父进程获取


进程的地址空间#

UNIX 中状态机的生命周期管理 API

  • fork execve _exit

  • 操作系统视角:状态机的复制、重置和删除

  • “创建新状态机” Spawn = fork + execve

int pid = fork();
if (pid == -1) { // 错误
    perror("fork"); goto fail;
} else if (pid == 0) { // 子进程
    execve(...);
    perror("execve"); exit(EXIT_FAILURE);
} else { // 父进程
    ...
    int status;
    waitpid(pid, &status, 0); // testkit.c 中有
}

6.1 进程的初始状态#

进程 execve 后的初始状态

(暂时忽略操作系统管理的状态: pid, 打开的文件……)

  1. 寄存器
  • gdb 中通过starti命令从执行第一条指令前停下,这时打印出来就行了

  • 包括 GPRs RIP(pc) RSP(sp)

  1. 内存

内存区域的权限通常由 4 个字符 表示

  • 前三个字符:表示内存的访问权限(读、写、执行)

    • r: Read(可读)

    • w: Write(可写)

    • x: Execute(可执行)

    • -: 无对应权限

  • 第四个字符:表示内存的 映射类型

    • p:Private(私有映射)

      • 写时复制(Copy-on-Write, COW)​ 仅在需要修改数据时,才真正复制原始数据的副本。在此之前,多个进程或线程可以共享同一份数据,而无需提前复制全部内容。
    • s:Shared(共享映射)

      • 多个进程共享同一物理内存页,修改会直接反映到其他进程或磁盘文件

(具体数值仅做参考,为运行address-space/alloc的结果)

  • 低地址( 0x1000 为 4KiB,内存页大小)

  • 0x400000–0x401000 0x1000 r--p

    • ELF 文件头和只读数据段(如 .rodata, .eh_frame
  • 0x401000–0x498000 0x97000 r-xp

    • 代码段(.text),包含程序的可执行指令
  • 0x498000–0x4c1000 0x29000 r--p

    • 只读数据段(如静态字符串、常量表)
  • 0x4c1000–0x4c8000 0x7000 rw-p

    • 数据段(.data.bss),存放全局变量和静态变量
  • 0x4c8000–0x4cd000 0x5000 rw-p

    • 堆(Heap)用于动态内存分配(如 malloc/free

    • 通过 brkmmap 系统调用动态增长


  • [vvar] 0x7ffff7ff9000-0x7ffff7ffd000 0x4000 r--p

    • 内核向用户空间暴露的只读数据(如系统时间、时钟频率),优化频繁读取的内核数据
  • [vdso] (Virtual Dynamic Shared Object) 0x7ffff7ffd000-0x7ffff7fff000 0x2000 r-xp

    • 在用户态执行某些系统调用(如 gettimeofday),避免陷入内核
  • [stack] 0x7ffffffdd000-0x7ffffffff000 0x22000 rw-p

    • 用户栈,存储函数调用链、局部变量和返回地址

    • 读写 ,不可执行(现代系统默认开启栈不可执行保护)

    • 从高地址向低地址扩展

  • [vsyscall] 0xffffffffff600000-0xffffffffff601000 0x1000 --xp

    • 旧版快速系统调用机制,现代系统已由 vdso 替代,保留仅为兼容

    • 无读写权限 ,仅执行(实际已弃用)

  • 高地址


6.2 进程的地址空间管理#

进程的初始只有ELF 文件里声明的内存和一些操作系统分配的内存

  • 任何其他指针的访问都是非法的

  • 如果我们从输入读一个 size,然后 malloc(size),内存从哪来呢?

  • 一定有一个系统调用可以改变进程的地址空间

Memory Map

// 映射
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
int munmap(void *addr, size_t length);

// 修改映射权限
int mprotect(void *addr, size_t length, int prot);
  • addr:建议的映射起始地址

    • 常设为 NULL 由内核自动选择
  • length:映射区域的长度(字节)

  • prot:内存保护标志(可读、可写、可执行、禁止访问)

  • flags:映射类型(匿名、私有、共享等)

    • MAP_ANONYMOUS匿名映射,不与任何文件关联的内存区域,通常用于动态分配内存(类似 malloc
  • fd:文件描述符(匿名映射时设为 -1)

  • offset:文件映射的偏移量(通常为 0)

int msync(void *addr, size_t length, int flags);
  • 默认情况下,操作系统会延迟将内存中的修改写回磁盘(通过页缓存机制),msync 允许用户强制同步

    • addr 必须是页对齐的(通常由 mmap 返回的地址直接使用)

    • 可通过 sysconf(_SC_PAGE_SIZE) 获取系统页大小

    • munmap 不会自动触发同步。未调用 sysnc 修改可能丢失

  • 确保其他进程或程序(如直接操作文件的进程)能看到最新的修改

    • MA_SYNC MS_ASYNC MS_INVALIDATE (同步,异步,使其他进程的映射失效)

用 pmap 命令查看进程的地址空间

pmap 查看进程的内存映射信息(如地址范围、权限、映射文件等)

读取 /proc/[pid]/maps 文件,该文件由内核生成,记录了进程的所有内存映射


  • 申请大量内存空间,瞬间完成

    • 但是物理内存的占用是延迟的(访问时触发缺页中断)

    • 操作系统分配内存不需要真的给你内存,所以申请很大的内存速度依然很快

    • malloc 对大内存(如超过 MMAP_THRESHOLD,默认 128KB)直接调用 mmap

缺页异常(Page Fault)是CPU在访问虚拟内存时,因页面未加载、权限不足或写时复制(写入共享内存页时,需复制新页以保持私有性)等原因触发的异常。

  • CPU 暂停当前指令,保存现场并切换到内核态

  • 应用场景

    • 动态内存分配:物理页延迟分配,触发缺页时再分配

    • 内存共享:多个进程共享同一物理页,减少冗余

    • 文件映射(mmap)​:访问文件时按需加载,避免一次性读取全部


  • 映射大文件、只访问其中的一小部分
with open('/dev/sda', 'rb') as fp:
    mm = mmap.mmap(fp.fileno(),
                   prot=mmap.PROT_READ, length=128 << 30)
    hexdump.hexdump(mm[:512])
  • 将硬盘设备文件 /dev/sda 的前 128GB 映射到内存,但仅读取前 512 字节

  • 无需将整个文件加载到内存,按需访问(操作系统自动处理分页)

  • 文件映射到内存后,读写操作通过内存地址直接完成,由操作系统负责与磁盘同步(若为 MAP_SHARED


6.3 入侵进程的地址空间#

进程 (状态机) 在 “无情执行指令机器” 上执行

  • 状态机是一个封闭世界

  • 但如果允许一个进程对其他进程的地址空间有访问权

    • 意味着可以任意改变另一个程序的行为

“入侵” 进程地址空间的例子

  • 调试器 (gdb),gdb 可以任意观测和修改程序的状态

金手指:直接物理劫持内存

  • 听起来很离谱,但 “卡带机” 时代的确可以做到!

  • 物理入侵进程地址空间

    • Game Genie: 一个 Look-up Table (LUT)

    • 简单、优雅:当 CPU 读地址 a 时读到 x,则替换为 y

  • 今天我们有 Debug Registers 和 Intel Processor Trace 帮助系统工具 “合法入侵” 地址空间


地址空间那么大,哪个才是 “金钱”?

  • 包含动态分配的内存,每次地址都不一样

  • 思路:Everything is a state machine

    • 观察状态机的 trace,筛选内存中符合其变化模式的地址,就知道哪个是金钱了

查找 + Filter

  • 进入游戏时 exp=4950

  • 打了个怪 exp=5100

  • 符合 4950→5100,4950→5100 变化的内存地址是很少

    • 好了,出门就是满级了

访问操作系统对象#

7.1 文件描述符#

文件描述符:文件描述符是指向操作系统对象的 “指针”(不恰当的比喻?)——系统调用通过这个指针 (fd) 确定进程希望访问操作系统中的哪个对象。我们有 open, close, read/write(解引用), lseek(指针内赋值/运算), dup(指针间赋值) 管理文件描述符。

int dup(int oldfd);

复制一个现有的文件描述符,生成一个新的描述符指向同一个文件/资源

int dup2(int oldfd, int newfd);

强制复制一个文件描述符到指定的数值。若目标描述符 newfd 已打开,则先关闭它,再复制


文件描述符的分配

总是分配最小的未使用描述符(最小未使用优先)

  • 0, 1, 2 是标准输入、输出和错误

  • 新打开的文件从 3 开始分配

    • 文件描述符是进程文件描述符表(进程内核数据结构的一部分)的索引

    • 关闭文件后,该描述符号可以被重新分配

进程可打开文件数限制

  • ulimit -n (进程限制)

  • sysctl fs.file-max (系统限制)

文件描述符中的 offset

文件描述符的 offset 是 “进程状态的” 的一部分,由操作系统维护;程序只能通过整数编号间接访问

Quiz: fork() 和 dup() 之后,文件描述符共享 offset 吗?

  • dup 新旧描述符会共享 offset,open 会得到独立的 offset

  • 父子进程的文件描述符独立维护 offset(写操作可能互相覆盖,需注意竞态)

Windows 中的文件描述符 Handle

  • 默认 handle 是不继承的 (和 UNIX 默认继承相反)

fcntl(fd, F_SETFD, FD_CLOEXEC)fcntl 用于对已打开的文件描述符进行底层控制


7.2 访问操作系统中的对象#

Filesystem Hierarchy Standard FHS

  • enables software and user to predict the location of installed files and directories

  • 例如 macOS 就不遵循 FHS

只要拷对了文件,操作系统就能正常执行啦!

  1. 创建 UEFI 分区,并复制正确的 Loader

  2. 创建文件系统

    • mkfs (格式化主分区)
  3. cp -ar 把文件正确复制 (保留权限)

    • 注意 fstab 里的 UUID(Universally Unique Identifier,每个磁盘分区在格式化时生成的唯一标识符)​

    • 复制系统文件到新硬盘后,必须用 blkid 获取新分区的 UUID,并更新到新系统的 /etc/fstab(系统启动时自动挂载磁盘分区的配置文件)中。如果配置错误,系统可能无法挂载根分区,导致启动失败

    • 你就得到了一个可以正常启动的系统盘!

  4. 运行时挂载必要的其他文件系统

    • 磁盘上的 /dev, /proc, … 都是空的

    • mount -t proc proc /mount/point 可以 “创建” procfs


任何 “可读写” 的东西都可以是文件(“一切皆文件”)

  1. 真实设备
  • 磁盘 /dev/sda 终端 /dev/tty
  1. 虚拟设备 (文件)
  • /dev/urandom (随机数), /dev/null (黑洞), …

    • 无实体文件,操作系统为它们实现了特别的 read 和 write 操作

    • 内核驱动(如 /drivers/char/mem.c

    • 可以通过 /sys/class/backlight 控制屏幕亮度

  • procfs 也是用类似的方式实现的

    • 进程/系统信息以文件形式暴露(如 /proc/cpuinfo
  1. 管道​:一个特殊的 “文件” ()
  • 特殊文件流,支持单向通信

  • 读端(read)和写端(write)分离

  1. 匿名管道int pipe(int pipefd[2]);
  • 返回两个文件描述符,配合 fork 使用(父子进程通信)

    • 先分配读端,再分配写端

    • 没有数据就会一直等待

非匿名管道 - int mkfifo(const char *pathname, mode_t mode);


“实现一切” 的基础知识

  1. 进程管理
  • fork, execve, waitpid, exit
  1. 内存管理
  • mmap, munmap, mprotect, msync
  1. 文件管理
  • open, close, read, write, lseek, dup

7.3 一切皆文件#

文件描述符适合什么?

字节流

  • 顺序读/顺序写

    • 没有数据时等待

    • 典型代表:管道

字节序列

  • 其实就有一点点不方便了

    • 需要到处 lseek 再 read/write

      • mmap 不香吗?指针指哪打哪

      • madvise, msync 提供了更精细的控制

int madvise(void *addr, size_t length, int advice);

向内核提供内存访问模式的建议,优化性能

  • MADV_SEQUENTIAL:预期会顺序访问(内核可提前预读或释放已用页)

  • MADV_RAMDOM:预期会随机访问(内核减少预读,避免浪费内存带宽)

  • MADV_DONTNEED:告知内核某些内存区域可提前释放

反思 Everything is a File

优点

  • 优雅,文本接口,就是好用

缺点

  • 和各种 API 紧密耦合

  • 对高速设备不够友好

    • 额外的延迟和内存拷贝

    • 单线程 I/O

终端和 UNIX Shell#

8.1 终端#

Some history:

打字机时代的遗产

  1. Shift
  • 使字锤或字模向上移动一段距离,切换字符集
  1. CR & LF
  • \r CR (Carriage Return): 回车,将打印头移回行首

    • print('Hel\rlo')
  • \n LF (Line Feed): 换行,将纸张向上移动一行

    • UNIX 的 \n 同时包含 CR 和 LF
  1. Tab & Backspace
  • 位置移动 (Backspace + 减号 = 错了划掉)

tty: from Teletypewriter 电传打字机(仍然无法纠错)

VT100: Video Teletypewriter (DEC, 1978)

  • 成为事实上的行业标准

    • 首个完整实现 ANSI Escape Sequence 的终端

      • Escape Sequence 指通过特定的字符组合(通常以 \ 开头)来表示无法直接输入或显示的字符,例如换行、引号、制表符等)
    • 80×24 字符显示成为标准布局


计算机(物理)终端:原理

  1. 作为输出设备
  • 接受 UART 信号并显示 (支持 Escape Sequence 就非常自然了)
  1. 作为输入设备
  • 把按键的 ASCII 码输出到 UART (所以有很多控制字符)

UART 是一种硬件设备,负责串行通信中的数据传输与格式转换。早期计算机通过 UART 连接物理终端(如电传打字机),实现输入与输出

伪终端 (Pseudo Terminal)和终端模拟器 (Terminal Emulator)

  1. 伪终端PTY:一对 “管道”(虚拟设备而非物理) 提供双向通信通道
  • 主设备 (PTY Master): 终端模拟器直接控制

  • 从设备 (PTY Slave): 连接到 shell 或其他程序(例如 /dev/pts/0

  • PTY也是内核管理的复杂对象

  1. 创建伪终端
  • openpty(): 通过 /dev/ptmx 申请一个新伪终端

    • 返回两个文件描述符 (master/slave)

如果直接在程序中访问 /dev/ptmx(即依赖linux的特性),那么代码就会不可移植

  • ssh, tmux new-window, ctrl-alt-t, …

    • ssh远程连接:本地终端模拟器通过主设备与远程Shell(从设备)通信
  1. 实现终端模拟器 openpty + fork
  • 子进程(如Shell进程):将 stdin/stdout/stderr 重定向(dut)至 slave

  • 父进程:从 master 读取子进程输出显示到屏幕; 将键盘输入写入 master,传递至子进程

  • Kitty 终端模拟器扩展 Escape Sequence 来显示图片

  • VSCode 的终端模拟器没有为不同操作系统单独开发底层终端逻辑,而是通过以下前后端分层实现跨平台兼容性

终端:更多功能

终端模式

  • Canonical Mode: 按行处理

    • 回车发送数据 (终端提供行编辑功能)
  • Non-canonical Mode: 按字符处理

    • 每个字符立即发送给程序

    • 用于实现交互式程序: vim, ssh

终端属性控制

  • tcgetattr()/tcsetattr() (<termios.h>)(用于控制终端设备的属性)

  • 可以控制终端的各种行为:回显、信号处理、特殊字符等

    • (你输密码的时候关闭了终端的回显)

8.2 终端和操作系统#

程序和终端的 “配对”

用户登录的起点

  • 系统启动 (内核 → init → getty)

    • getty 打开终端设备(如 /dev/tty1),后续的 login 程序和 Shell 继承该终端的文件描述符

    • fork() 会继承文件描述符 (指针),因此,子进程也会指向同一个终端

    • 通过 Ctrl+Alt+F1~F12 切换不同的虚拟控制台(如 tty1tty12)

  • 远程登录 (sshd → fork → openpty)

    • Shell 进程的 stdin, stdout, stderr 都会指向分配的slave端
  • vscode (fork → openpty)

CLI 到 GUI

  • CLI: 用户直接与物理终端或虚拟控制台(tty)交互,通过 getty/login 启动 Shell。

  • GUI: 终端模拟器,本质是图形应用程序,通过创建 pty 模拟终端行为

    • 虽然运行在 GUI 中,但终端模拟器仍依赖内核的 pty 机制与 Shell 通信

进程管理:要解决的问题

我们有那么大一棵进程树,都指向同一个终端,有的在前台,有的在后台,Ctrl-C 到底终止哪个进程?

答案:终端才不管呢

  • 它只管传输字符

    • Ctrl-C: End of Text (ETX), \x03

    • Ctrl-D: End of Transmission (EOT), \x04(退出python,cat)

    • Ctrl-\: quit SIGQUIT

    • stty -a: 可以看到按键绑定

      • stty (set teminal type) 用于显示和修改终端行设置
  • 操作系统收到了这个字符,就可以对 “当前” 的进程采取行动,给这个进程发信号

作为操作系统的设计者,需要在收到 Ctrl-C 的时候找到一个 “当前进程”

你会怎么做?

  • fork() 会产生树状结构 (还有托孤行为)

  • Ctrl-C 应该终止所有前台的 “进程们”,但不能误伤后台的 “进程们”


会话 (Session) 和进程组 (Process Group):机制

操作系统怎么知道ctrl-c终止谁?

给进程引入一个额外编号 (Session ID,大分组)

  • 子进程会继承父进程的 Session ID(SID,Session Leader(通常是登录shell)的pid)

  • 一个 Session 关联一个控制终端 (controlling terminal,如/dev/pts/0),包含 foreground PGID和 controlling SID

  • Leader 退出时,全体进程收到 Hang Up (SIGHUP)

再引入另一个编号 (Process Group ID,小分组)

  • 进程组是一组相关进程的集合(如一条命令及其子进程)

  • 只能有一个前台进程组

    • 其他进程组为后台进程组,无法直接接收终端输入
  • 操作系统收到 Ctrl-C,向前台进程组所有进程发送 SIGINT

  • Ctrl-Z,SIGTSTP 将job并放到后台;jobs,查看后台进程;fg x,SIGCONT 将x号程序回到前台

  • Leader 退出时,进程组仍存在,直到所有进程退出


会话和进程组:API

太不优雅了

  • setsid()/getsid(pid)

    • setsid 会脱离 controlling terminal
  • setpgid(pid, pgid)/getpgid(pid)

  • tcsetpgrp(fd, pgid)/tcgetpgrp(fd)

    • 将终端(文件描述符 fd)的前台进程组设置为 pgid

    • 获取终端的前台进程组 ID

    • 迷惑 API

以及……uid, effective uid (?), saved uid (???)

  • uid:进程的实际所有者。

  • euid:用于权限检查(如文件访问)。

  • suid:允许程序临时提升权限后恢复原权限。

  • Setuid Demystified

是的,历史的糟粕

最后:Ctrl-C 到底做了什么?

  1. 终端驱动Ctrl-C 转换为 SIGINT,发送给前台进程组。

  2. 操作系统在目标进程返回用户态时,强制插入信号处理逻辑:

    • 若无自定义处理函数,默认终止进程。

    • 若有处理函数,模拟一次函数调用,执行后恢复原状态。

进程可以注册自定义函数(如 signal(SIGINT, handler))来响应信号;使用 sigaction 替代传统的 UNIX signal 机制。


8.3 UNIX Shell 编程语言#

语言机制

  • 预处理: $() (命令替换), <() (进程替代,将命令的输出作为文件描述符提供给其他命令 diff)

  • 重定向: cmd > file < file 2> /dev/null

    • 2> /dev/null 将 cmd 的标准错误输出(stderr)重定向到 /dev/null
  • 顺序结构: cmd1; cmd2, cmd1 && cmd2, cmd1 || cmd2

    • 顺序,短路机制
  • 管道: cmd1 | cmd2

    • 这些命令被翻译成系统调用序列 (open, dup, pipe, fork, execve, waitpid, …)

例子:实现重定向

有趣的例子

  • ls > a.txt | cat

    • 我已经重定向给 a.txt 了,cat 是不是就收不到输入了?
  • bash/zsh 的行为是不同的(前者收不到)

    • 所以脚本用 #!/bin/bash 甚至 #!/bin/sh 保持兼容
NJU-OS 笔记 week3-4
https://durjustice.github.io/homepage/posts/nju-os/week3-4/
作者
Durjustice
发布于
2025-03-25
许可协议
CC BY-NC-SA 4.0