My-CSAPP

Exceptional Control Flow

Execeptional Control Flow

Control Flow

CPU 随着时间顺序执行不同的指令, from startup to shutdown.

两个可以在 program state 修改 control flow

但是,对于计算机而言,需要system state的 control flow 的切换: Exceptional Control Flow

Execeptional Control Flow

Exceptions

用户程序在运行中,抛出异常,程序的控制流转移到 Kernel Code,内核中的 Exception Handler 可以确定下一步的状态:

在内核代码中,具体的异常处理指令的跳转指令是固定的,具体由一张 Exception Tables 进行查询。

Asynchronous Interrupts

上述的异常机制叫做同步异常,程序在运行 user code 的过程中遇到异常,将程序的控制流直接转移到内核代码。而异步异常是由外部硬件事件随机触发、与当前执行的指令无关的中断信号,CPU 必须在”任意时刻”暂停当前程序去处理它。

例如:

Synchronous Exceptions

System Calls

在汇编代码中,系统会直接使用 syscall 代表进行 system calls,%rax 存储了 sys-call 的编号(每一个编号对应不同 sys-call 的操作,例如读文件,写文件没打开关闭文件,创建/终止/杀死一个进程,运行一个程序等等)

在操作系统的视角,sys_call 被特定的函数封装。

Fault

Processes

Def: A process is an instance of a running program.

Process provides each program with 2 abstractions:

进程是资源分配的最小单位(内存、文件、FD 等),线程是CPU 调度的最小单位(执行流)。

实际计算机的运行过程是多进程的,并行的运行很多进程:

在多核 CPU 上,可能存在多个 CPU + registers,context switching 的情况仍然也会发生,每一个核都可以执行一个单独的进程。

Concurrent Processes

并发的定义: Flows overlap in time.

在并发的过程中,CPU 可能会中断执行 Process A 的运行,在中途运行 Process B, 则两个进程之间是并发的,因为从时间上两个进程之间产生了 Overlap.

Otherwise: sequential

Context Switching

在并行执行的进程之间,不同的 Process 之间如何切换?通过内核代码实现 Context Switch.

Process Control

System Call Error Handling

System Call 利用返回值来判断执行的状态是否成功。

On error, Linux system-­level func&ons typically return ‐1 and set global variable errno to indicate cause.

例如,在 C 代码中 fork 一个新的进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <errno.h>

int main() {
    pid_t pid;
    
    if ((pid = fork()) < 0) {
        // fork 失败
        fprintf(stderr, "fork error: %s\n", strerror(errno));
        exit(1);
    }
    else if (pid == 0) {
        printf("Hello from CHILD process!\n");
        printf("Child PID: %d\n", getpid());
        printf("Parent PID: %d\n", getppid());
        exit(0);
    }
    else {
        printf("Hello from PARENT process!\n");
        printf("Parent PID: %d\n", getpid());
        printf("Child PID: %d\n", pid);
        
        wait(NULL);
        printf("Child process finished.\n");
    }
    
    return 0;
}

系统级编程和用户级编程的理念存在很大的差异,系统级编程中,内核中任何微小的 bug 都可能会被快速放大,造成严重的错误,因此,对于那些内核函数,一旦检测到返回值为异常返回值,就会立刻中断程序。而对于用户级编程而言,存在更加高级易用的异常处理机制。

当然,操作系统封装了对应的操作函数,也自带了 Error Handling 的机制。

Creating and Terminating Processes

创建进程的方式可以调用 fork 函数:

通过 Fork 创建的子进程在初始化时会复制父进程的大部分上下文:

写时复制(COW) 是一种资源管理优化技术:多个进程共享同一块内存,只有当某个进程试图“修改”这块内存时,系统才会真正复制一份新的给它。是一种延迟复制来减少资源占用的机制。

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

pid_t Fork(void) {
    pid_t pid;
    
    if ((pid = fork()) < 0) {
        fprintf(stderr, "Fork error: %s\n", strerror(errno));
        exit(0);
    }
    
    return pid;
}

int main(){
  pid_t pid;
  int x = 1;
  pid = Fork();
  if (pid == 0){
    // child
    printf("Child : x=%d\n", ++x);
    exit(0);
  }

  // parent
  printf("Parent : x=%d\n", --x);
  exit(0);
}
Parent : x=0
Child : x=2

Process Graph

我们使用一个树来建模不同进程之间的依赖关系:

对上述的进程树进行拓扑排序,可以得到一个启动进程的序列。

void fork_2(){
  printf("L0\n");
  fork();
  printf("L1\n");
  fork();
  printf("L2\n");
  fork();
  printf("L3\n");
}

例如,该程序的运行结果如下:

L0
L1
L1
L2
L2
L2
L2
L3
L3
L3
L3
L3
L3
L3
L3

每一次 fork,父进程都会创建一个新的子进程,同时父进程本身也持续执行代码,因此,在 3 此 fork 之后,就会存在 8 个正在执行的进程。

void fork_4() {
  printf("L0\n");
  if (fork() != 0) {
    // * 只对父进程进行
    printf("L1\n");
    if (fork() != 0) {
      // 只对父进程执行
      printf("L2\n");
    }
  }
  printf("L3\n");
}
L0
L1
L3
L2
L3
L3

Reaping Processes

无论进程何时终止,内核会在回收阶段回收已经终止的进程。当子进程结束时,操作系统不会立刻把它的所有信息都删掉。它会保留一点点信息,直到它的父进程来查看。

Reap 的方式: 父进程调用 wait() 函数

因此,总结一下:

wait: Synchronizing with Children

wait 函数是一个同步函数,父进程在调用之后,父进程的运行状态被阻塞,并等待任意一个子进程结束,在结束后父进程恢复执行,并且子进程的资源被内核释放。

void fork_9_nonwait() {
  if (fork() == 0) {
    printf("L0\n");
    exit(0);
  } else {
    printf("L1\n");
    printf("L2\n");
  }
  printf("L3\n");
}

void fork_9_wait() {
  int child_status;
  if (fork() == 0) {
    printf("L0\n");
    exit(0);
  } else {
    printf("L1\n");
    wait(&child_status);
    printf("L2\n");
  }
  printf("L3\n");
}
=== fork_9_nonwait
L1
L2
L3
L0
=== fork_9_wait
L1
L0
* L2 和 L3 被阻塞,优先等待子进程运行完成
L2
L3

execev: Loading and Running Programs

这个函数可以在当前进程中读取并运行一个程序:

int execve(char *filename, char *argv[], char *envp[])

三个参数:

execve 不会创建新进程,而是替换当前进程的代码、数据和栈。

// 可以是二进制可执行文件
execve("/bin/ls", ...);

// 也可以是脚本文件(以 #! 开头)
execve("./script.sh", ...);  // 文件首行:#!/bin/bash
// 例如执行:ls -l /home
char *argv[] = { "ls", "-l", "/home", NULL };
execve("/bin/ls", argv, envp);

// 约定:argv[0] 是程序名本身

execve 只会被调用一次,并且不会有返回值。

在调用 execve 之后,当前的 PID 的数据段和代码会被替换:

在实际运行时,通常会选择新建一个 child,在一个 child process 中运行 execev 函数:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>

int main() {
    pid_t pid;
    int status;
    
    // 1. 创建子进程
    pid = fork();
    
    if (pid < 0) {
        // fork 失败
        fprintf(stderr, "Fork failed: %s\n", strerror(errno));
        exit(1);
    }
    else if (pid == 0) {
        // ============================================
        // 子进程代码
        // ============================================
        printf("[Child %d] Starting...\n", getpid());
        printf("[Child %d] Current directory: ", getpid());
        
        // 获取并打印当前工作目录
        char cwd[1024];
        if (getcwd(cwd, sizeof(cwd)) != NULL) {
            printf("%s\n", cwd);
        }
        
        // 准备 execve 的参数
        // 相当于执行:ls -l
        char *filename = "/bin/ls";
        char *argv[] = { "ls", "-l", NULL };  // argv[0] 约定为程序名
        char *envp[] = { NULL };               
        
        printf("[Child %d] Executing: %s\n", getpid(), filename);
        
        execve(filename, argv, envp);
        
        fprintf(stderr, "[Child %d] execve failed: %s\n", getpid(), strerror(errno));
        exit(127);  
    }
    else {
        printf("[Parent %d] Child created with PID %d\n", getpid(), pid);
        printf("[Parent %d] Waiting for child to complete...\n", getpid());
        
        // 4. 等待子进程结束并回收资源
        pid_t wpid = wait(&status);
        
        // 5. 检查子进程退出状态
        if (WIFEXITED(status)) {
            int exit_code = WEXITSTATUS(status);
            printf("[Parent %d] Child %d exited normally with status %d\n", 
                   getpid(), wpid, exit_code);
            
            if (exit_code == 0) {
                printf("[Parent %d] ✅ Child completed successfully!\n", getpid());
            } else {
                printf("[Parent %d] ⚠️  Child completed with error code %d\n", 
                       getpid(), exit_code);
            }
        }
        else if (WIFSIGNALED(status)) {
            printf("[Parent %d] Child %d was killed by signal %d\n", 
                   getpid(), wpid, WTERMSIG(status));
        }
        
        printf("[Parent %d] All done, exiting.\n", getpid());
    }
    
    return 0;
}
[Parent 32680] Child created with PID 32682
[Parent 32680] Waiting for child to complete...
[Child 32682] Starting...
[Child 32682] Current directory: /Users/xiyuanyang/Desktop/Dev/CSAPP
[Child 32682] Executing: /bin/ls
total 40
-rw-r--r--@  1 xiyuanyang  staff    97 Feb 18 21:25 Dockerfile
-rw-r--r--@  1 xiyuanyang  staff  4223 Mar 17 00:02 README.md
drwxr-xr-x@ 67 xiyuanyang  staff  2144 Mar 24 12:13 build
drwxr-xr-x@  8 xiyuanyang  staff   256 Mar 24 08:29 docs
-rw-r--r--@  1 xiyuanyang  staff   975 Jan  9 17:05 makefile
-rw-r--r--@  1 xiyuanyang  staff    32 Jan  9 14:04 run.sh
drwxr-xr-x@  4 xiyuanyang  staff   128 Feb 18 21:29 scripts
drwxr-xr-x@ 28 xiyuanyang  staff   896 Mar 21 16:56 slides
drwxr-xr-x@ 17 xiyuanyang  staff   544 Mar 24 10:31 src
[Parent 32680] Child 32682 exited normally with status 0
[Parent 32680] ✅ Child completed successfully!
[Parent 32680] All done, exiting.

Signals

Shells

Shell: an application program that runs programs on behalf of the user:

int main() {
  char cmdline[MAXLINE];
  while (1) {
    // read
    fgets(cmdline, MAXLINE, stdin);
    if (feof(stdin)) {
      exit(0);
    }

    // evaluate
    eval(cmdline);
  }
}

Shell 执行的功能是和用户的字符串输入进行交互,并将对应的指令解析、发送到操作系统内核。

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#define MAXLINE 100
#define MAXARGS 100
extern char **environ;

void unix_error(char *msg) {
  fprintf(stderr, "%s: %s\n", msg, strerror(errno));
  exit(1);
}

pid_t Fork(void) {
  pid_t pid;

  if ((pid = fork()) < 0) {
    fprintf(stderr, "Fork error: %s\n", strerror(errno));
    exit(0);
  }

  return pid;
}

int parseline(char *buf, char **argv) {
  char *delim; /* Points to first space delimiter */
  int argc;    /* Number of args */
  int bg;      /* Background job? */

  buf[strlen(buf) - 1] = ' ';   /* Replace trailing '\n' with space */
  while (*buf && (*buf == ' ')) /* Ignore leading spaces */
    buf++;

  /* Build the argv list */
  argc = 0;
  while ((delim = strchr(buf, ' '))) {
    argv[argc++] = buf;
    *delim = '\0';
    buf = delim + 1;
    while (*buf && (*buf == ' ')) /* Ignore spaces */
      buf++;
  }
  argv[argc] = NULL;

  if (argc == 0) /* Ignore blank line */
    return 1;

  /* Should the job run in the background? */
  if ((bg = (*argv[argc - 1] == '&')) != 0)
    argv[--argc] = NULL;

  return bg;
}

int builtin_command(char **argv) {
  if (!strcmp(argv[0], "quit")) /* quit command */
    exit(0);
  if (!strcmp(argv[0], "&")) /* Ignore singleton & */
    return 1;

  return 0; /* Not a builtin command */
}

void eval(char *cmdline) {
  char *argv[MAXARGS]; /* Argument list execve() */
  char buf[MAXLINE];   /* Holds modified command line */
  int bg;              /* Should the job run in bg or fg? */
  pid_t pid;           /* Process id */

  strcpy(buf, cmdline);
  bg = parseline(buf, argv);
  if (argv[0] == NULL)
    return; /* Ignore empty lines */

  if (!builtin_command(argv)) {
    if ((pid = Fork()) == 0) { /* Child runs user job */
      if (execve(argv[0], argv, environ) < 0) {
        printf("%s: Command not found.\n", argv[0]);
        exit(0);
      }
    }

    /* Parent waits for foreground job to terminate */
    if (!bg) {
      int status;
      if (waitpid(pid, &status, 0) < 0)
        unix_error("waitfg: waitpid error");
    } else
      printf("%d %s", pid, cmdline);
  }
  return;
}

int main() {
  char cmdline[MAXLINE];
  while (1) {
    // read
    fgets(cmdline, MAXLINE, stdin);
    if (feof(stdin)) {
      exit(0);
    }

    // evaluate
    eval(cmdline);
  }
}

bash run.sh src/Lecture14/shellex.c

/bin/ls
build           docs            README.md       scripts         src
Dockerfile      makefile        run.sh          slides
/bin/pwd
/Users/xiyuanyang/Desktop/Dev/CSAPP
/usr/bin/whoami
xiyuanyang
quit

可以看到,我们写了一个非常简单的 eval 函数,可以实现 shell 的最基本的一些功能。

Signals

Signal: a small message that nofifies a process that an event of some type has occured in the systems:

Sending a Signal

Kernel sends (delivers) a signal to a destination process by updating some state in the context of the destination process. Linux 系统没有额外设计一套复杂的内核和进程之间的通信机制来进行 signals 的传输和接受,内核并没有向进程的内存空间写入任何数据,也没有打断进程当前的指令流。内核只是在该进程对应的 进程控制块(PCB,Linux 中是 task_struct) 中,将某个比特位(bit)从 0 改为 1。

Receiving a Signal

Non-local Jumps