美文网首页
2019-01-01 CSAPP 第八章(二)

2019-01-01 CSAPP 第八章(二)

作者: ShawnPanCn | 来源:发表于2019-01-01 23:00 被阅读0次

    8.5 信号

    研究一种更高层次的软件形式的异常, 也是一种软件中断,称为Unix信号,它允许进程中断其他进程。

    一个信号就是一条小消息,它通知进程系统中发生一个某种类型的事件。

    Linux系统支持30多种信号。

    每种信号类型对应于某种系统事件

    底层的信号。

    当底层发生硬件异常,信号通知 用户进程 发生了这些异常。

    除以0:发送SIGILL信号。

    非法存储器引用:发送SIGSEGV信号

    较高层次的软件事件

    键入ctrl+c:发送SIGINT信号

    一个进程可以发送给另一个进程SIGKILL信号强制终止它。

    子进程终止或者停止,内核会发送一个SIGCHLD信号给父进程。

    8.5.1 信号术语

    传送一个信号到目的进程有两个步骤。

    发送信号: 内核通过更新目的进程上下文的某个状态,就说发送一个信号给目的进程。

    发送信号有两个原因

    内核检测到一个系统事件。比如被零除错误,或者子进程终止。

    一个进程调用了kill函数。显示要求进程发送信号给目的进程。

    一个进程可以发信号给它自己。

    接收信号: 当目的进程 被内核强迫以某种方式对信号的发送做出反应。目的进程就接收了信号。

    进程可以忽略这个信号,终止。

    或者通过一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。

    一个只发出而没有被接收的信号叫做待处理信号(pending signal)

    一种类型至多只有一个待处理信号。

    如果一个进程有一个类型为k的待处理信号。

    那么接下来发送到这个进程类型为k的信号都会被简单的丢弃。

    一个进程可以有选择性地阻塞接收某种信号

    它任然可以被发送。但是产生的待处理信号不会被接收。

    一个待处理信号最多被接收一次。内核为每个进程在pending位向量维护着待处理信号的集合,而在blocked位向量维护着被阻塞的信号集合。只要传送一个类型为k的信号,内核就会设置pending中的第k位,而只要接收了一个类型为k的信号,内核就会清除pending中的第k位。

    8.5.2 发送信号

    Unix系统 提供大量向进程发送信号的机制。所有这些机制都是基于进程组(process group)。

    进程组

    每个进程都属于一个进程组。

    由一个正整数进程组ID来标示

    getpgrp()函数返回当前进程的进程组ID:

    #include<unistd.h>

    pid_t getpgrp(void);

    1

    2

    默认,一个子进程和它的父进程同属于一个进程组

    一个进程可以通过setpgid()来改变自己或者其他进程的进程组。

    #include<unistd.h>

    int setpgid(pid_t pid,pid_t pgid);

    如果pid是0 ,那么使用当前进程的pid。

    如果pgid是0,那么使用指定的pid作为pgid(即pgid=pid)。

    例如:进程15213调用setpgid(0,0)

    那么进程15213会 创建/加入进程组15213.

    1

    2

    3

    4

    5

    6

    7

    用/bin/kill 程序发送信号

    /bin/kill可以向另外的进程发送任意的信号。

    比如

    unix>/bin/kill -9 15213

    1

    发送信号9(SIGKILL)给进程15213。

    一个为负的PID会导致信号被发送到进程组PID中的每个进程。

    unix>/bin/kill -9 -15213

    1

    发送信号9(SIGKILL)给进程组15213中的每个进程。

    用/bin/kill的原因是,有些Unix shell 有自己的kill命令

    从键盘发送信号

    作业(job) :对一个命令行求值而创建的进程。

    在任何时候至多只有一个前台作业和0个或多个后台作业

    前台作业就是需要等待的

    后台作业就是不需要等待的

    键入unix>ls|sort

    创建一个两个进程组成的前台作业。

    两个进程通过Unix管道链接。

    shell为每个作业创建了一个独立的进程组。

    进程组ID取自作业中父进程中的一个。

    在键盘输入ctrl-c 会发送一个SIGINT信号到外壳。外壳捕获该信号。然后发送SIGINT信号到这个前台进程组的每个进程。在默认情况下,结果是终止前台作业

    类似,输入ctrl-z会发送一个SIGTSTP信号到外壳,外壳捕获这个信号,并发送SIGTSTP信号给前台进程组的每个进程,在默认情况,结果是停止(挂起)前台作业(还是僵死的)

    用kill函数发送信号

    进程通过调用kill函数发送信号给其他进程,类似于bin/kill

    int kill(pid_t pid, int sig);

    1

    pid>0,发送信号sig给进程pid

    pid<0,发送信号sig给进程组abs(pid)

    事例:kill(pid,SIGKILL)

    用alarm函数发送信号

    进程可以通过调用alarm函数向它自己SIGALRM信号。

    #include<unistd.h>

    unsigned int alarm(unsigned int secs);

    返回:前一次闹钟剩余的秒数。

    1

    2

    3

    4

    5

    alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程

    如果secs=0 那么不会调度闹钟,当然不会发送SIGALRM信号。

    在任何情况,对alarm的调用会取消待处理(pending)的闹钟,并且会返回被取消的闹钟还剩余多少秒结束。如果没有pending的话,返回0

    一个例子:

    输出

    unix> ./alarm

    BEEP

    BEEP

    BEEP

    BEEP

    BEEP

    BOOM!

    //handler是一个自己定义的信号处理程序,通过signal函数捆绑。

    1

    2

    3

    4

    5

    6

    7

    8

    8.5.3 接收信号

    信号的处理时机是在从内核态切换到用户态时,会执行do_signal()函数来处理信号

    当内核从一个异常处理程序返回,准备将控制传递给进程p时,它会检查进程p的未被阻塞的待处理信号的集合(pening&~blocked)。

    如果这个集合为空,内核将控制传递到p的逻辑控制流的下一条指令。

    如果非空,内核选择集合中某个信号k(通常是最小的k),并且强制p接收k。收到这个信号会触发进程某些行为。一旦进程完成行为,传递到p的逻辑控制流的下一条指令。

    每个信号类型都有一个预定义的默认类型,以下几种.

    进程终止

    进程终止并转储存器(dump core)

    进程停止直到被SIGCONT信号重启

    进程忽略该信号

    进程可以通过使用signal函数修改和信号相关联的默认行为。

    SIGSTOP,SIGKILL是不能被修改的例外。

    #include<signal.h>

    typedef void (*sighandler_t)(int);

    sighandler_t signal(int signum,sighandler_t handler);

    1

    2

    3

    4

    signal函数通过下列三种方式之一改变和信号signum相关联的行为。

    如果handler是SIG_IGN,那么忽略类型为signum的信号

    如果handler是SIG_DFL,那么类型为signum的信号恢复为默认行为。

    否则,handler就是用户定义的函数地址,这个函数称为信号处理程序

    只要进程接收到一个类型为signum的信号,就会调用handler。

    设置信号处理程序:把函数传递给signal改变信号的默认行为。

    调用信号处理程序,叫捕获信号

    执行信号处理程序,叫处理信号

    当处理程序执行它的return语句后,控制通常传递回控制流中进程被信号接收中断位置处的指令。

    信号处理程序是计算机并发的又一个示例。信号处理程序的执行中断,类似于底层异常处理程序中断当前应用程序的控制流的方式。因为信号处理程序的逻辑控制流与主函数的逻辑控制流重叠,信号处理程序和主函数并发地运行。

    自我思考:信号是一种异常/中断,当接收到信号的时候,会停下当前进程所做的事,立马去执行信号处理程序。并不是多线程/并行,但确是并发的。从下面这张图,可见一斑。

    8.5.4 信号处理问题

    当一个程序要捕获多个信号时,一些细微的问题就产生了。

    待处理信号被阻塞

    Unix 信号处理程序通常会阻塞 当前处理程序正在处理 的类型的待处理信号。

    待处理信号(被抛弃了)不会排队等待

    当有两个同类型信号都是待处理信号时,有一个会被抛弃。

    关键思想:存在一个待处理的信号k仅仅表明至少一个一个信号k到达过。

    系统调用可以被中断(在某些unix系统会出现)

    像read,wait和accept这样的系统调用潜在的阻塞一段较长的时间,称为慢速系统调用。

    当处理程序捕获一个信号,被中断的慢速系统调用在信号处理程序返回后将不在继续,而是立即返回给用户一个错误条件,并将errno设置为EINTR。

    用一个后台回收僵死子进程的程序,前台读入做例子

    1.初始简单利用接收SIGCHLD信号回收,一次调用只回收一个。

    在调用的过程中,又有信号发送过来,但是被阻塞了。之后又被直接抛弃。

    如果不处理被阻塞和不会排队等待的问题。会有信号被抛弃。

    重要教训:不可以用信号对其他进程中发送的事件计数

    handle1-code

    2.一次调用尽可能的多回收,保证在回收过程中,没有遗漏的信号。

    handle2-code

    3.还存在一个问题,在前台中,某些unix系统(Solaris系统)的read被中断后不会自动重启,需要手动重启,Linux一般会自动重启。

    之前 read模块 code

    现在改为如果是errno==EINTR手动重启。

    或者使用Signal包装函数标准。8.5.5会提到。

    8.5.5 可移植的信号处理

    不同系统之间,信号处理语义的差异(比如一个被中断的慢速系统调用是重启,还是永久放弃)是Unix信号系统的一个缺陷。

    为了处理这个问题,Posix标准定义了sigaction函数,它允许与Linux和Solaris这样与Posix兼容的系统上的用户,明确指明他们想要的信号处理语义。

    #include<signal.h>

    int sigaction(int signum,stuct sigaction *act,struct sigaction *oldcat);

    //若成功则为1,出错则为-1。

    1

    2

    3

    4

    sigaction函数应用不广泛,它要求用户设置多个结构条目。

    一个更简洁的方式,是定义一个包装函数,称为Signal,它调用sigaction。

    它的调用方式与signal函数的调用方式一样。

    Signal包装函数设置了一个信号处理程序,其信号处理语义如下(设置标准):

    只有这个处理程序当前正在处理的那种类型被阻塞。

    和所有信号实现一样,信号不会排队等待。

    只要可能,被中断的系统调用会自动重启

    一旦设置了信号处理程序,它就会一直保持,直到Signal带着handler参数为SIG_IGN或者SIG_DFL被调用。

    在某些比较老的Unix系统,信号处理程序被使用一次后,又回到默认行为。

    8.5.6 显示地阻塞和取消阻塞信号

    通过sigprocmask函数来操作。

    #include<signal.h>

    int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);

    1

    2

    3

    sigprocmask函数改变当前已阻塞信号的集合(8.5.1节描述的blocked位向量)。

    具体行为依赖how值

    SIG_BLOCK:添加set中的信号到blocked中。

    SIG_UNBLOCK: 从blocked删除set中的信号。

    SIG_SETMASK: blocked=set。

    如果oldset非空,block位向量以前的值会保存到oldset中。

    还有以下函数操作set集合

    #include<signal.h>

    int sigemptyset(sigset_t *set);

    //置空

    int sigfillset(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。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    8.5.7 同步流以避免讨厌的并发错误

    如何编写读写相同存储位置的并发流程序的问题,困扰着数代计算机科学家。

    流可能交错 的数量是与指令数 量呈指数关系

    有些交错会产生正确结果,有些可能不会。

    所谓同步流就是。以某种方式同步并发流,从而得到 最大的可行交错的集合 ,每个交错集合都能得到正确的结果。

    并发编程是一个很深奥,很重要的问题。在第12章详细讨论。

    现在我们只考虑一个并发相关的智力挑战。

    code

    如果发生以下情况,会出现同步错误。

    父进程执行fork函数,内核调度新创建的子进程运行,而不是父进程。

    在父进程再次运行前,子进程已经终止,变成僵死进程,需要内核一个SIGCHLD信号给父进程

    父进程处理信号,调用deletejob.

    调用addjob。

    显然deletejob必须在addjob之后,不然添加进去的job永久存在。这就是同步错误。

    这是一个称为竞争(race)的经典同步错误的示例。

    main中的addjob和处理程序中调用deletejob之间存在竞争。

    必须addjob赢得进展,结果才是正确的,否则就是错误的。但是addjob不一定能赢,所以有可能错误。即为同步错误。

    因为内核的调度问题,这种错误十分难以被发现。难以调试。

    Q:如何消除竞争?

    A:可以在fork之前,阻塞SIGCHLD信号,在调用addjob后取消阻塞。

    注意,子进程继承了阻塞,我们要小心地接触子进程中的阻塞。

    消除竞争的原则就是,让该赢得竞争的对象在任何情况下都能赢。

    一个暴露你的代码中竞争的简便技巧

    制造一个fork的包装函数Fork,通过随机+休眠,在fork的那一瞬间,让子进程,父进程都有50%机会先运行

    8.6 非本地跳转

    C语言提供一种用户级异常控制流形式,称为非本地跳转(nonlocal jump)。

    它将控制直接从一个函数转移到另一个当前正在执行的函数。不需要经过正常的调用-返回序列。

    非本地跳转是通过setjmp和longjmp函数来提供。

    #include<setjmp.h>

    int setjmp(jmp_buf env);

    int sigsetjmp(sigjmp_buf env,int savesigs);//信号处理程序使用

    //参数savesigs若为非0则代表搁置的信号集合也会一块保存

    1

    2

    3

    4

    5

    setjmp函数在env缓冲区保存当前调用环境,以供后面longjmp使用,并返回0

    调用环境包括程序计数器,栈指针,通用目的寄存器。

    #include

    8.7 操作进程的工具

    STRACE(痕迹):打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。

    用-static编译,能得到一个更干净,不带有大量共享库相关的输出的轨迹。

    PS(Processes Status): 列出当前系统的进程(包括僵死进程)

    TOP(因为我们关注峰值的几个程序,所以叫TOP):打印当前进程使用的信息。

    PMAP(rePort Memory map of A Process): 查看进程的内存映像信息

    /proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数据结构。

    用户程序可以读取这些内容。

    比如,输入"cat /proc/loadavg,观察Linux系统上当前的平均负载。

    8.8 小结

    异常控制流(ECF)发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。

    在硬件层,异常是处理器中的事件出发的控制流中的突变。控制流传递给一个异常处理程序,该处理程序进行一些处理,然后返回控制被中断的控制流。

    有四种不同类型的异常:中断,故障,终止和陷阱。

    定时器芯片或磁盘控制器,设置了处理器芯片上的中断引脚时,中断会异步发生。返回到Inext

    一条指令的执行可能导致故障和终止同时出现。

    故障可能返回调用指令。

    终止不将控制返回。

    陷阱用于系统调用。结束后,返回Inext

    在操作系统层,内核用ECF提供进程的基本概念。进程给应用两个重要抽象:

    逻辑控制流

    私有地址空间

    在操作系统和应用程序接口处,有子进程,和信号。

    最后,C语言的非本地跳转 完成应用程序层面的异常处理。

    至此,异常贯穿了从底层硬件,到抽象的软件层次。

    相关文章

      网友评论

          本文标题:2019-01-01 CSAPP 第八章(二)

          本文链接:https://www.haomeiwen.com/subject/mwoolqtx.html