CS:APP 异常控制流 学习笔记

本文深入探讨了异常控制流的概念,包括中断、陷阱、故障和终止的处理方式,以及Linux/x86-64系统中的异常处理。同时,详细介绍了进程的定义、上下文切换、多任务、私有地址空间等概念,以及如何在Linux系统中创建、终止和控制进程。

8.1异常

异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。
异常就是控制流中的突变,用来响应处理器状态中的某些变化

异常处理

过程调用与异常的不同之处

  • 过程调用时,在跳转到处理程序之前,处理器将返回地址压入栈中。 而异常处理根据类型,返回地址可能是当前指令或者下一条指令
  • 异常处理时,处理器把一些额外的处理器状态压到栈里,在处理器返回时,重新开始执行被中断的程序需要这些状态
  • 如果控制从用户程序转移到内核,所有的这些项目丢被压到内核栈中,而不是压到用户栈中。
  • 异常处理程序运行在内核模式下,对所有系统资源都有完全的访问权限

异常的类别

类别原因同步/异步返回行为
中断来自I/O设备的信号异步总是返回下一条指令
陷阱有意的异常同步总是返回下一条指令
故障潜在可恢复的错误同步可能返回当前指令
终止不可恢复的错误同步不会返回
  • 中断: 中断是异步发生的,是来自处理器外部I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。
  • 陷阱: 陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途就是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用
  • 故障: 故障由错误情况引起,它可能能够被故障处理程序修正。
  • 终止: 终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。

Linux/x86-64系统中的异常

  1. Linux/x86-64 故障和终止
  • 除法错误(Floating exception)
    应用试图除零,一个除法指令的结果对于目标操作数来说过大。
  • 一般保护故障(Segmentation fault)
    引用未定义的虚拟内存区域,试图写只读文本段
  • 缺页
    异常处理程序将适当的磁盘上虚拟内存的一个页面映射到物理内存的一个页面,然后重新执行触发异常的指令
  • 机器检查
    导致故障的指令执行中检查到致命的硬件错误时发成的。机器检查处理程序从不返回控制给应用程序
  1. Linux/x86-64 系统调用

8.2 进程

进程(process) 的经典定义就是一个执行中程序的实例。系统中每个程序都运行在每个进程的上下文中。

上下文(context) 是由程序正确运行所需的状态组成。这个状态包括内存中程序的代码和数据,栈、通用寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

逻辑控制流

程序运行时PC值的序列叫做逻辑控制流,简称逻辑流

并发流

一个逻辑流的执行在时间上与另一个流重叠,称为并发流(concurrent flow)

多个流并发地执行的一般现象称为并发(concurrency)

一个进程和其他进程轮流运行的概念称为多任务(multitasking)

一个进程执行它的控制流的一部分的每一个时间段叫做时间片 (time slice) ,多任务也叫时间分片(time slicing)

两个流如果并发地运行在不同的处理器核或者计算机上,我们称它为并行流(parallel flow) ,他们并发地运行(running parallel),并行地执行(parallel execution)。

私有地址空间

进程位每个程序提供它自己私有地址空间,一般而言,和这个地址空间相关联的内存字节不不能被其他进程读或者写。

用户模式和内核模式

当设置了模式位时,进程就运行在内核模式中,可以执行指令集中的任何命令和访问系统中的任何内存位置。

没有设置模式位时,进程就运行在用户模式中,不允许执行特权指令(privileged instructions),比如停止处理器,改位模式,发起一个I/O操作,也不允许直接引用内存空间中内核区的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据

上下文切换

操作系统内核使用一种称为上下文切换(context switch)的较高形式的异常控制流来实现多任务。

8.4 进程控制

获取进程ID

#include <sys/types.h>
#include <unistd.h>
pid_t getpid();   //当前进程的pid
pid_t getppid();  //父进程的pid

Example

#include "fork.h"
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	pid_t pid = Fork();
	if (pid > 0){
		printf("father:\n");
		printf("pid=%d ppid=%d\n",getpid(),getppid());
	}
	else{
		printf("child\n");
		printf("pid=%d ppid=%d\n",getpid(),getppid());
	}
	return 0;
}

创建和终止进程

终止进程

#include <stdlib.h>
int exit(int status);

创建新的运行的子进程

#include <sys/types.h>
#include <unistd.h>
pid_t fork();//子进程返回0,父进程返回子进程的PID,出错返回-1
  • fork() 调用一次,返回两次 父进程返回子进程的PID,子进程返回0
  • 并发执行 父进程和子进程是并发运行的独立进程
  • 相同但是独立的地址空间
  • 共享文件 子进程继承父进程打开的文件

回收子进程

当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除,进程被保持在一种已经终止的状态中,知道被它的父进程回收(reaped)

当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已经终止的进程。

终止了但未被回收的进程称为僵死进程(zombie),急事僵死进程没有运行,他们仍然消耗系统内存资源。

如果父进程终止了,内核会安排init进程回收子进程。

waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *satausp, int options);

参数

  1. 等待集合成员pid

    • 如果pid>0,等待集合为一个单独的子进程,它的进程id等于pid
    • 如果pid=-1,那么等待集合位父进程所有的子进程。
    • 其他
  2. 修改默认行为options

    • WNOHANG: 如果等待集合中任何子进程都还没有终止,就立即返回0。
    • WUNTRACED: 挂起调用进程,直到等待集合中的一个进程变成已终止或者已被停止。返回的pid为导致返回的已终止或者被停止的子进程的PID。
    • WCOUNTINUED: 挂起调用进程,直到等待集合中的一个进程变成已终止或者等待集合中一个被停止的进程收到SIGCOUNT信号重新开始。
  3. 检查已回收子进程的退出状态

    • WIFEXITED(status): 如果子进程通过调用exit或者返回(return)正常返回,就返回真
    • WEXITSTATUS(status): 返回一个正常终止子进程的退出状态。 只有WIFEXITED为真时才会定义这个状态
    • WIFSIGNALED(status):如果子进程是因为一个违背捕获的信号终止的,那么就返回真
    • WTERMSIG(status):返回导致子进程终止的信号的编号。只有WIFSIGNALED返回真时才定义这个状态
    • WIFSTOPPED(status):如果引起返回的子进程当前是停止的,那么就返回真
    • WSTOPSIG(status):返回引起子进程停止的信号的编号。只有在WIFSTOPPED返回真时才定义这个状态。
    • WIFCONTINUED(status):如果子进程收到SIGCONT信号重新启动就返回真

wait函数

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status):

等价于waitpid(-1,&status,0)

进程休眠

sleep
#include <unistd.h>
unsigned int sleep(unsigned int secs);

如果请求的时间量已经到了,返回0;
否则返回剩下要休眠的秒数(sleep被一个信号中断过早的返回)

pause
#include <unistd.h>
int pause(void);

休眠直到接收到一个信号,总是返回-1

加载并运行程序execve

#include <unistd.h>
int execve(const char *filename,const char *argv[],const char *envp[]);

如果成功不返回,否则返回-1

参数

  • filename 要加载并运行的可执行目标文件
  • argv 参数列表
  • envp 环境变量
#include <stdlib>
char *getenv(const char *name); //返回指向name的指针,不存在返回NULL
setenv(const char *name,const char *newvalue,int overwrite);//成功返回0,否则返回-1;
//在overwrite非零时覆盖旧值
void unsetenv(const char *name);//删除环境变量

8.5 信号

在这里插入图片描述

信号术语

  • 发送信号:内核通过更新目的进程上下文的某个状态,发送一个信号给目的进程。
    发送信号可能有以下两种原因:
    • 内核检测到一个系统事件,比如除零错误或者子进程终止
    • 一个进程调用额kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己
  • 接收信号:当目的进程被内核强迫以某种方式对信号的发出做出反应时,它就接收了信号
  • 待处理信号(pending signal):一个发出而没有被接受的信号

发送信号

进程组

每个进程只属于一个进程组

#include <unistd.h>
pid_t getpgrp(); //返回调用进程的进程组ID

把ID为pid的进程的组ID改为pgid
int setpgid(pid_t pid,pid_t pgid);   //设置进程的进程组ID,成功返回0,否则返回-1

setpid:

  • pid=0,更改当前进程的PID
  • gpid=0,设置组ID为进程的pid
用kill函数发送信号
#include <unistd.h>
#include <sys/types.h>
int kill(pid_t pid, int sig); //成功返回0,否则返回-1
  • pid>0,发送sig信号给进程pid
  • pid=0,发送sig信号给调用进程所在进程组中的每个进程
  • pid<0,发送sig信号给进程组|pid|中的每个进程
用alarm函数发送信号

alarm函数向调用进程发送SIGALRM信号

#include <unistd.h>
unsigned int alarm(unsigned int secs); 
//安排内核在secs秒后发送一个SIGALRM信号给调用进程
//返回前一个闹钟剩余秒数,没有返回0

接收信号

当内核把进程p从内核模式切换到用户模式时,它会检查进程p未被阻塞的待处理信号集合(pending & ~ blocked),如果非空,内核会选择某个信号k,并强制内核接收信号k。

进程收到信号后会触发某种行为。每个信号类型都有一个预定义的默认行为:

  • 进程终止
  • 进程终止并转储
  • 进程停止(挂起)直到被SIGCONT信号重启
sighandler_t
#include <gignal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
//成功返回前次指向处理程序的指针, 否则返回SIG_ERR(不设置errno)

signal通过以下方式修改和信号signum相关联的行为:

  • 若handler是SIG_IGN,忽略类型为signum的信号
  • 若handler是SIG_DFL,恢复类型位signum信号的默认行为
  • 否则handler为用户定义的函数地址,这个函数称为信号处理程序。

阻塞和解除阻塞信号

Linux提供阻塞信号的隐式和显示的机制

  • 隐式阻塞机制。 内核阻塞任何当前处理程序正在处理信号类型的待处理信号。
  • 显示阻塞机制。 调用sigprocmask函数和它的辅助函数
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfullset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
//成功返回0,否则返回-1

int sigismember(const sigset_t *set, int signum);
//若signum是set的成员返回1,不是返回0,否则返回-1

sigprocmask函数改变当前阻塞的信号的集合。
how:

  • SIG_BLOCK:把set中的信号添加到blocked中
  • SIG_UNBLOCK:从blcoked中删除set中的信号
  • SIG_SETMASK:blocked=set
    如果oldset非空,oldset被设置为blocked被修改之前的值

编写信号处理程序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值