ECF

Exceptional Control Flow(异常控制流)

处理器检测到异常时,通过异常表调用异常处理程序进行处理。
可以通过

  • 跳转和分支
  • 调用和返回

改变控制流。但是当系统状态发生变化时,需要ECF进行处理。

  • 硬件层:异常(图8-1)
    ECF_第1张图片
    处理器状态发生变化(event)时,通过异常表调用相应的异常处理程序(运行在内核模式下)。
    • 中断:异步异常。由处理器外面发生的事件引起的,例如计时器中断和 I/O 中断。返回后执行下一条指令。
    • 陷阱:同步异常。向用户提供系统调用(用户程序和内核的接口)调用相应的内核程序。返回后执行下一条指令。
      在 x86-64 系统中,每个系统调用都有一个唯一的 ID。参数通过通用寄存器传递。使用syscall指令进行系统调用。
    • 故障:同步异常。程序发生错误时,处理器将控制交给故障处理程序。如果处理成功,重新执行该指令;否则终止该指令所在程序。
    • 终止:同步异常。由不可恢复的错误造成,直接退出当前的程序。
  • 操作系统层:进程切换
    进程:

    • 逻辑控制流:
      上下文
      ECF_第2张图片
      ECF_第3张图片
      切换的内核机制让每个程序都感觉自己在独占处理器
      ECF_第4张图片
      进程切换:内核通过调度器(代码)调度进程,进程轮流执行逻辑流的一部分
      ECF_第5张图片
      并发:伪并行执行(例如进程A只在进程C执行的间隙执行,则称进程AC并发)
      并行:使用多个 CPU 同时执行
    • 私有地址空间:
      通过虚拟内存机制让每个程序都感觉自己在独占内存

    • 进程状态:
      运行:正在或者等待被执行
      停止:执行被挂起,在进一步通知前不会计划执行
      终止:进程被永久停止

    • 进程组:
      每个进程只属于1个进程组
      父子进程同属1个进程组
getpgrp() //返回当前进程所在进程组ID
setpgid(pid_t pid,pid_t pgid) //设置一个进程的进程组CP528
  • 进程控制:
    1.(系统调用的)错误处理:使用错误处理包装函数简化代码
pid_t Fork(void)//包装函数
{
    pid_t pid;
    //Unix的系统级函数遇到错误时会返回-1
    if ( (pid = fork()) < 0 )
        unix_error("Fork error");
    return pid;
}

void unix_error(char *msg) //错误处理函数 
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(0);
}

2.获取进程信息:

  • pid_t getpid(void) :返回当前进程的 PID(进程ID)
  • pid_t getppid(void) : 返回当前进程的父进程的 PID

3.创建进程:
父进程通过fork函数(返回该进程的子进程PID,返回2次)调用子进程(父进程的副本)
ECF_第6张图片
在父进程中;在新创建的子进程中,fork函数返回0
子进程共享父进程打开的文件
父子进程是并发运行的独立进程(有独立的地址空间)
execve函数在当前进程中加载并运行1个新程序(程序运行在进程的上下文中)。注意新程序的PID不变,覆盖当前的地址空间,继承调用函数时已打开的文件。

4.子进程回收
当进程终止后,内核会中断常规执行并通过信号通知。只有被其父进程回收时,系统才会清除该进程。
如果父进程在回收子进程之前被终止,内核通过init进程回收

  • 应用层:

    • 信号
      一种异步的通知机制,用来提醒进程一个事件已经发生。
      1.当内核检测到进程发生某种事件,内核将对应的信号发送给该进程:
      2.进程调用kill函数,显式要求内核发送信号给该进程。

    发送信号:内核更新目的进程上下文的某个状态

/* /bin/kill程序发送信号 */
linux> /bin/kill -9 24818//发送信号9给进程24818
linux> /bin/kill -9 24818//发送信号9给进程组24818的每个进程
/* kill函数发送信号 */
int kill(pid_t pid,int sig);//CP530

键盘发送信号
Ctrl+C:内核终止前台作业
Ctrl+Z:内核挂起前台作业

接收信号:操作系统中断了进程正常的控制流程。
每个信号类型都有默认行为:

  • 忽略这个信号
  • 终止进程
  • 捕获信号,执行信号处理器

进程可以通过signal函数修改默认行为。

//函数指针
typedef void(*sighandler_t)(int);
//设置信号处理函数
sighandler_t *signal(int signum, handler_t *handler);
void sigint_handler(int sig) // SIGINT 处理器
{
    printf("不能通过ctrl+c关闭\n");
    exit(0);
}
int main()
{
    // 接收到由于Ctrl+C发出的信号,调用sigint_handler作为信号处理函数
    if (signal(SIGINT, sigint_handler) == SIG_ERR)
        unix_error("signal error");

    // 等待接收信号
    pause();
    return 0;
}

阻塞信号:

  • 隐式阻塞机制:当信号处理程序正在运行时,内核默认阻塞同一类型的信号
  • 显式阻塞机制:使用 sigprocmask 函数以及其他辅助函数CP533

信号处理器也可以被其他的信号处理器中断。
等待状态:信号已被发送但是未被接收。同类型的信号至多只会有一个待处理信号。
信号处理器和主程序并行且共享相同的全局数据结构,尤其要注意因为并行访问可能导致的数据损坏的问题。基本的指南:

  1. 规则 1
    信号处理器越简单越好
  2. 规则 2
    信号处理器中只调用异步且信号安全(async-signal-safe)的函数
  3. 规则 3
    在进入和退出的时候保存和恢复 errno
    这样信号处理器就不会覆盖原有的 errno 值
  4. 规则 4
    临时阻塞所有的信号以保证对于共享数据结构的访问
    防止可能出现的数据损坏
  5. 规则 5:
    用 volatile 关键字声明全局变量
    编译器就不会把它们保存在寄存器中,保证一致性
  6. 规则 6
    用 volatile sig_atomic_t 来声明全局标识符(flag)
    这样可以防止出现访问异常

  7. 非本地跳转:从一个函数跳转到另一个函数中
    本地跳转:在一个程序中通过 goto 语句进行流程跳转

//保存当前程序的堆栈上下文环境
/*仅在调用 setjmp 的函数内有效,如果调用 setjmp 的函数返回则失效。*/
int setjmp(jmp_buf env);

/*恢复由 setjmp 保存的程序堆栈上下文,即程序从调用 setjmp 处重新开始执行*/
int longjmp(jmp_buf env,int retval);

setjmp 函数被调用1次,返回2次。
1. 第1次调用setjmp 函数返回0;
2. 调用longjmp函数返回retval

允许从深层嵌套中避开调用栈直接返回。
(C++的try语句属于该函数更高级的版本,catch语句类似于setjmp 函数,throw语句类似于longjmp函数)

你可能感兴趣的:(CSAPP,异常处理,异常)