20135323符运锦--信息安全系统设计基础第十一周学习总结
学习计时:8小时
读书:3
代码:2
作业:1
博客:2
第八章 异常控制流
现代系统通过使控制流发生突变来对这些情况作出反应。一般而言,我们把这些突变称为异常控制流。
8.1异常
异常时异常控制流的一种形式,它一部分是由硬件实现的,一部分是由操作系统实现的。说白了,异常就是控制流的突变,用来响应处理器状态中的某些变化。下图为其基本思想
当前指令为:ICURR。
在处理器中,状态呗编码为不同的位和信号。状态变化称为事件。事件可能和当前指令的执行直接相关。
当处理器检测到有事发生时,会通过异常表的跳转表,进行一个简洁过程调用,到一个专门设计用来处理这类事件的操作系统子程序。
当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况的一种:
8.1.1异常处理
在系统启动时,操作系统分配和初始化一张称为异常表的跳转表,使得条目K包含异常K的处理程序的地址,以下为一张异常表的格式
异常类似于过程调用,但是有一些重要的不同之处
·过程调用时,在跳转到处理程序之前,处理器将返回地址压人找中。然而,根据异常的类型,返回地址要么是当前指令(当事件发生时正在执行的指令),要么是下一条指令(如果事件不发生,将会在当前指令后执行的指令〉。
·处理器也把一些额外的处理器状态压到楼里,在处理程序返回时,重新开始被中断的程序会需要这些状态。比如,一个IA32 系统将包含当前条件码和其他内容的EFLAGS 寄存器压人栈中。
·如果控制从一个用户程序转移到内核,那么所有这些项目都被压到内核棋中,而不是压到用户楼中。
·异常处理程序运行在内核模式下,这意味着它们对所有的系统资源都有完全的访问权限。
8.1.2 异常的类别
1.中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。
硬件中断的异常处理程序通常称为中断处理程序。
中断处理的过程如下:
当前指令完成后,处理器注意到中断引脚的电压变化,所以从系统总线读取异常号,调用适当的中断处理程序。当处理程序返回时,它就将程序返回给下一条指令。
2.陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。
处理器提供一条特殊的SYSCALL N指令,当用户程序想要请求服务N时,可以执行这条指令。
执行SYSCALL指令会导致一个到异常处理程序的陷阱,这个处理程序对参数解码,并调用适当的内核程序。以下是一个系统调用的处理:
系统调用运行在内核模式中,内核模式允许系统调用执行命令,并访问定义在内核中的栈。
3.故障
故障由错误情况引起,它可能能够被故障处理程序修正。
故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误,那么它将控制返回给引起故障的指令,从而重新执行它。
否则,处理程序返回到内核的ABORT例程,ABORT例程会终止引起故障的程序
4.终止
终止时不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM和SRAM位被损坏时发生的奇偶错误。
8.1.3LINUX/IA32系统中的异常
1.LINUX/IA32故障和终止
①.除法错误。当应用试图除以0时,或者当一个除法指令的结果对于目标操作数来说太大了的时候,就会发生此错误。
②.一般保护故障。通常是因为一个程序引用了一个未定义的虚拟存储器区域或者因为程序试图写一个只读的文本段。
③.缺页。是会重新执行产生故障的指令的一个异常示例.
④.机器检查.是在导致故障的指令执行中检测到致命的硬件错误时发生的.
LINUX/IA32系统调用
8.2进程
进程的经典定义就是一个执行中的程序的实例.系统中的每个程序都是运行在某个进程的上下文中的。上下文是由程序正确运行所需的状态组成的。
每次用户通过向外壳输入一个可执行目标文件的名字,并运行一个程序时,外壳就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
应用程序的关键抽象:
·一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
·一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用存储器系统。
8.2.1逻辑控制流
如果想用调试器单步执行程序,我们会看到一些列的程序计数器(PC)的值,这些值唯一的对应于包含在程序的可执行目标文件的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值得序列叫做逻辑控制流。
每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。
8.2.2并发流
一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。
多个流并发地执行的一般现象称为并发。一个进程和其他进程轮流运行的概念称为多任务. 一个进程执行它的控制流的一部分的每一时间段叫做时间片 。因此,多任务也叫做时间分片 。果两个流并发地运行在不同的处理器核或者计算机上,那么我们称它为并行流,它们并行地运行且执行。
8.2.3私有地址空间
一个进程为每个程序提供它自己的私有地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读或者写的。
地址空间顶部是保留给内核的。地址空间的这个部分包含内核在代表进程执行指令时使用的代码、数据和栈。
8.2.4用户模式和内核模式
没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。因此,用户程序必须通过系统调用接口间接地访问内核代码和数据。
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序码时,处理器就把模式从内核模式改回到用户模式。
Linux 提供了一种聪明的机制,叫做/proc 文件系统,它允许用户模式进程访问内核数据结构的内容。/proc 文件系统将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。比如,你可以使用/proc 文件系统找出一般的系统属性
8.2.5上下文切换
上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成。
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定就叫做调度( schedule ) ,是由内核中称为调度器( scheduler )的代码处理的。当内核选择一个新的进程运行时,我们就说内核调皮了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为土下丈切换的机制来将控制转移到新的进程,上下
文切换1)保存当前进程的上下文, 2) 恢复某个先前被抢占的进程被保存的上下文, 3) 将控
制传递给这个新恢复的进程。
当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为某个等待时间发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。中断也可以发生上下文切换。
8.4进程控制
8.4.1获取进程ID
每个进程都有一个唯一的正数进程ID(PID)。getpid函数返回调用进程的PID。getppid函数返回它的父进程的PID。
8.4.2创建和终止进程
进程总是处于下面三种状态之一:
①运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
②停止。进程的执行被挂起,且不会被调度。当收到SIGSTOP\SIGTSTP
\SIDTTIN或者SIGTTOU信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行。
③终止。进程永远的停止了。进程会因为三种原因停止:1.收到一个信号,该信号的默认行为是终止进程。2.从主程序返回3.调用EXIT函数
新创建的子进程几乎但不完全与父进程相同。当父进程调用FORK时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
FORK函数只被调用一次,却会返回两次:一次是在调用父进程中,一次是在新创建的子进程中。在父进程中,FORK返回子进程的PID。在子进程中,FORK返回0.因为子进程的PID总是非零的,返回值就提供一个明确的方法来分辨程序是在父进程还是子进程中执行的。
例子:
8.4.3回收子进程
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。
当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个终止了但还未被回收的进程称为僵死进程。
一个进程可以通过调用WAITPID函数来等待它的子进程终止或者停止。
1.判断等待集合的成员
等待集合的成员是由参数PID来确定的:
①如果PID>0,那么等待集合就是一个单独的子进程,它的进程ID等于PID。
②如果PID=—1,那么等待集合就是由父进程所有的子进程组成的。
2.修改默认行为
可以通过将optioins 设置为常量WNOHANG 和WUNTRACED 的各种组合,修改默认行为:
.WNOHANG: 如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。
• WUNTRACED :挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回的PID 为导致返回的己终止或被停止子进程的PID。默认的行为是只返回己终止的子进程。当你想要检查已终止和被停止的子进程时,这个选项会有用。
.WNOHANG阳UNTRACED: 立即返回,如果等待集合中没有任何子进程被停止或已终止,那么返回值为0 ,或者返回值等于那个被停止或者己终止的子进程的PID 。
3.检查已回收子进程的退出状态
4.错误条件
如果调用进程没有子进程,那么WAITPID返回-1,并且设置ERRNO为ECHILD。如果WAITPID函数被一个信号中断,那么它返回-1.并设置ERRNO为EINTR.
5.wait函数
8.4.4让进程休眠
sleep函数将一个进程挂起一段8-56111717.png)
指定的时间。
如果请求的时间量已经到了,以上函数返回0,否则返回还剩下的要休眠的秒数。
PAUSE函数:让调用函数休眠,直到该进程收到一个信号
8.4.5加载并运行程序
execve函数在当前进程的上下文中加载并运行一个新程序
参数列表由下图表示.ARGV变量指向一个以NULL结尾的指针数组,其中每个指针都指向一个参数串。ARGV[0]是可执行目标文件的名字。
ENVP变量指向一个以NULL结尾的指针数组,其中每个指针指向一个环境变量串。
函数的主函数如下:
int main(int argc,char **argv,char **envp)
等价于其数组形式
UNIX提供了几个函数来操作环境数组:
getenv函数在环境数组中搜索字符串"name = value".如果找到了,返回一个指向VALUE的指针,否则返回NULL。
补充:外壳是一个交互型的应用级程序,它代表用户运行其他程序。最早的外壳是SH程序,后面更加丰富。外壳执行一系列的读、求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户执行程序。
数组指针、指针数组、函数指针、指针函数的区别
例如:
int *p[4]; //指针数组。 是个有4个元素的数组, 每个元素的是指向整型的指针。(数组的每个元素都是指针)
int (*p)[4]; //数组指针。 它是一个指针,指向有4个整型元素的数组。 (一个指针指向有4个整型元素的数组)
int func(void); //指针函数。 无参函数, 返回整型指针。 (函数的返回值为int)
int (*func)(void); //表示函数指针,可以指向无参, 且返回值为整型指针的函数。 (函数的返回值为int)
8.5信号
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。
在外壳命令上输入man 7 signal得到这30中不同类型的信号
8.5.1信号术语
传送一个信号到目的进程是由两个不同的步骤组成的:
①发送信号。发送一个信号给目的进程。发送信号可以有如下原因:1)内核检测到一个系统事件,比如被零除错误或者子进程终结。2)一个人进程调用KILL函数,显示地要求一个信号给目的进程
②接收信号。当目的进程被内核强迫以某种方式对信号的发送作出反应时,目的进程就接受了信号。终止或者通过执行一个称谓信号处理程序的用户层函数捕获这个信号。
如果一个进程有一个类型为K的待处理信号,那么任何接下来发送到这个进程的类型为K的信号都不会排队等待,它们只是被简单地丢弃。
8.5.2发送信号
1.进程组
进程组是由一个正整数进程组ID来标识的。GETPGRP函数返回当前进程的进程组ID:
setgpid函数将进程PID的进程组改为PGID。如果PID是0,那么就使用当前进程的PID。
2.从键盘发送信号
UNIX外壳使用作业这个抽象概念来表示为对一个命令行求值而创建的进程。创建一个由两个进程组成的前台作业,这两个进程是通过UNIX管道连接起来的:一个进程运行LS程序,另一个运行SORT程序。
在键盘上输入ctrl-c会导致一个SIGINT信号被发送到外壳。
3.用KILL函数发送信号
进程通过调用KILL函数发送信号给其他进程
如果PID大于0,那么KILL函数发送信号SIG给进程PID。如果PID小于0,那么KILL发送信号给SIG给进程组ABS中的每个进程。
用ALARM函数发送信号
进程可以通过调用ALARM函数向它自己发送SIGALRM
8.5.3接收信号
当内核从一个异常处理程序返回,准备将控制传递给进程p 时,它会检查进程p 的未被阻塞的待处理信号的集合(pending&-blocked) 。如果这个集合为空(通常情况下),那么内核将控制传递到p 的逻辑控制流中的下一条指令
然而,如果集合是非空的,那么内核选择集合中的某个信号k ( 通常是最小的k) , 并且强制p 接收信号k. 收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回p 的逻辑控制流中的下一条指令(]next) 。每个信号类型都有一个预定义的默认行为,是下面中的一种:
·进程终止。
·进程终止并转储存储器(dump core) 。
.进程停止直到被SIGCONT 信号重启.
.进程忽略该信号。
signal 函数可以通过下列三种方法之一来改变和信号signum 相关联的行为:
·如果handler 是SIG_IGN. 那么忽略类型为signum 的信号。
·如果handler 是SIG_DFL,那么类型为signum 的信号行为恢复为默认行为。
·否则, handler 就是用户定义的函数的地址,这个函数称为信号处理程序(signalhandler) ,只要进程接收到一个类型为signurn 的信号,就会调用这个程序。通过把处理程序的地址传递到signal 函数从而改变默认行为,这叫做设置信号处理程序( installing the handler) 。调用信号处理程序称为捕获信号。执行信号处理程序称为处理信号。
8.5.4信号处理问题
对于只捕获一个信号并终止的程序来说,信号处理是简单直接的。然而,当一个程序要捕获多个信号时,一些细微的问题就产生了。
·待处理信号被阻塞。Unix 信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。比如,假设一个进程捕获了一个SIGINT 信号,并且当前正在运行它的SIGINT 处理程序。如果另一个SIGINT 信号传递到这个进程,那么这个SIGINT 将变成待处理的,但是不会被接收,直到处理程序返回。
·待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为k 的信号传送到一个目的进程,而由于目的进程当前正在执行信号k 的处理程序,所以信号k 是阻塞的,那么第二个信号就被简单地丢弃,它不会排队等待。关键思想是存在一个待处理的信号仅仅表明至少已经有一个信号到达了。
·系统调用可以被中断。像read 、wait 和accept 这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误条件,并将errno。设置为EINTR。
运行结果:linux> ./signa11
Hello from child 10320
Hello from child 10321
Hello from child 10322
Ha且dler reaped chi1d 10320
Ha且dler reaped child 10322
processing input
8.5.5可移植的信号处理
不同系统之间,信号处理语义的差异〈比如一个被中断的慢速系统调用是重启还是永久放弃)是Unix 信号处理的一个缺陷。为了处理这个问题, Posix 标准定义了sigaction 函数,它允许像Linux 和Solaris 这样与Posix 兼容的系统上的用户,明确地指定他们想要的信号处理语义。
sigaction 函数运用并不广泛,因为它要求用户设置多个结构条目。一个要简洁的方式,最初是由W.Richard Stevens 提出的[109] ,就是定义一个包装函数,称为Signal ,它调用sigaction 。Signal 的定义,它的调用方式与signal 函数的调用方式一样。Signal 包装函数设置了一个信号处理程序,其信号处理语义如下:
·只有这个处理程序当前正在处理的那种类型的信号被阻塞。
·和所有信号实现一样,信号不会排队等待。
·只要可能,被中断的系统调用会自动重启。
·一旦设置了信号处理程序,它就会一直保持,直到Signal 带着handler 参数为SIG_IGN 或者SIG.:_DFL 被调用。(一些比较老的'Unix 系统会在一个处理程序处理完一个信号之后,将信号行为恢复为它的默认行为。
8.5.6显式地阻塞和取消阻塞信号
应用程序可以使用sigprocmask 函数显式地阻塞和取消阻塞选择的信号:
sigprocmask 函数改变当前已阻塞信号的集合(8.5.1 节中描述的blocked 位向量〉。具体的行为依赖于how 的值:
• SIG_BLOCK: 添加set 中的信号到blocked 中(blocked = blocked I set).
• SIg_.UNBLOCK: 从blocked 中删除set 中的信号(blocked = blocked & -set) 。
• SIG_SEtMASK : blocked = set 。
如果。ldset 非空, blocked 位向量以前的值会保存在oldset 中。
可以使用下列函数操作像set 这样的信号集合。sigemptyset 初始化set 为空集。
sigfillset 函数将每个信号添加到set 中。sigaddset 函数添加signum 到set , sigdelset从set 中删除signum,如果signum 是set 的成员,那么sigismember 返回1 ,否则返回0。
8.5.7同步流以避免讨厌的并发错误
当父进程创建一个新的子进程时,它就把这个子进程添加到作业列表中。当父进程在SIGCHLD 处理程序中回收一个终止的〈僵死〉子进程时,它就从作业列表中删除这个子进程。乍一看,这段代码看上去是对的。不幸的是,可能发生下面的情况:
1)父进程执行fork 函数,内核调度新创建的子进程运行,而不是父进程。
-
在父进程能够再次运行之前,子进程就终止,并且变成一个僵死进程,使得内核传递一个SIGCHLD 信号给父进程。
-
后来,当父进程再次变成可运行但又在它执行之前,内核注意到待处理的SIGCHLD 信号,并通过在父进程中运行处理程序接收这个信号
4)处理程序回收终止的子进程,并调用deletejob ,这个函数什么也不做,因为父进程还没有把该子进程添加到列表中。
5)在处理程序运行完毕后,内核运行父进程,父进程从fork 返回,通过调用addjob 错误地把(不存在的〉子进程添加到作业列表中。
8.6非本地跳转
非本地跳转(nonlocal jump) ,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。非本
地跳转是通过setjmp和longjmp 函数来提供的。
setjmp 函数只被调用一次,但返回多次:一次是当第一次调用setjmp ,而调用环境保存在缓冲区env 中时:一次是为每个相应的longjmp 调用。另一方面, longjmp 函数被调用一次,但从不返回。
参考资料
《深入理解计算机系统》
遇到的问题及解决
书本中有很多的代码举例加深对函数的理解,但是不是每个函数都能吃透,希望老师能够讲解。例如有的例子涉及并发编程就不是很理解,我也会查阅更多资料去解决这个问题学习计时:8小时
读书:3
代码:2
作业:1
博客:2
第八章 异常控制流
现代系统通过使控制流发生突变来对这些情况作出反应。一般而言,我们把这些突变称为异常控制流。
8.1异常
异常时异常控制流的一种形式,它一部分是由硬件实现的,一部分是由操作系统实现的。说白了,异常就是控制流的突变,用来响应处理器状态中的某些变化。下图为其基本思想
当前指令为:ICURR。
在处理器中,状态呗编码为不同的位和信号。状态变化称为事件。事件可能和当前指令的执行直接相关。
当处理器检测到有事发生时,会通过异常表的跳转表,进行一个简洁过程调用,到一个专门设计用来处理这类事件的操作系统子程序。
当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况的一种:
8.1.1异常处理
在系统启动时,操作系统分配和初始化一张称为异常表的跳转表,使得条目K包含异常K的处理程序的地址,以下为一张异常表的格式
异常类似于过程调用,但是有一些重要的不同之处
·过程调用时,在跳转到处理程序之前,处理器将返回地址压人找中。然而,根据异常的类型,返回地址要么是当前指令(当事件发生时正在执行的指令),要么是下一条指令(如果事件不发生,将会在当前指令后执行的指令〉。
·处理器也把一些额外的处理器状态压到楼里,在处理程序返回时,重新开始被中断的程序会需要这些状态。比如,一个IA32 系统将包含当前条件码和其他内容的EFLAGS 寄存器压人栈中。
·如果控制从一个用户程序转移到内核,那么所有这些项目都被压到内核棋中,而不是压到用户楼中。
·异常处理程序运行在内核模式下,这意味着它们对所有的系统资源都有完全的访问权限。
8.1.2 异常的类别
1.中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。
硬件中断的异常处理程序通常称为中断处理程序。
中断处理的过程如下:
当前指令完成后,处理器注意到中断引脚的电压变化,所以从系统总线读取异常号,调用适当的中断处理程序。当处理程序返回时,它就将程序返回给下一条指令。
2.陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。
处理器提供一条特殊的SYSCALL N指令,当用户程序想要请求服务N时,可以执行这条指令。
执行SYSCALL指令会导致一个到异常处理程序的陷阱,这个处理程序对参数解码,并调用适当的内核程序。以下是一个系统调用的处理:
系统调用运行在内核模式中,内核模式允许系统调用执行命令,并访问定义在内核中的栈。
3.故障
故障由错误情况引起,它可能能够被故障处理程序修正。
故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误,那么它将控制返回给引起故障的指令,从而重新执行它。
否则,处理程序返回到内核的ABORT例程,ABORT例程会终止引起故障的程序
4.终止
终止时不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM和SRAM位被损坏时发生的奇偶错误。
8.1.3LINUX/IA32系统中的异常
1.LINUX/IA32故障和终止
①.除法错误。当应用试图除以0时,或者当一个除法指令的结果对于目标操作数来说太大了的时候,就会发生此错误。
②.一般保护故障。通常是因为一个程序引用了一个未定义的虚拟存储器区域或者因为程序试图写一个只读的文本段。
③.缺页。是会重新执行产生故障的指令的一个异常示例.
④.机器检查.是在导致故障的指令执行中检测到致命的硬件错误时发生的.
LINUX/IA32系统调用
8.2进程
进程的经典定义就是一个执行中的程序的实例.系统中的每个程序都是运行在某个进程的上下文中的。上下文是由程序正确运行所需的状态组成的。
每次用户通过向外壳输入一个可执行目标文件的名字,并运行一个程序时,外壳就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
应用程序的关键抽象:
·一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
·一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用存储器系统。
8.2.1逻辑控制流
如果想用调试器单步执行程序,我们会看到一些列的程序计数器(PC)的值,这些值唯一的对应于包含在程序的可执行目标文件的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值得序列叫做逻辑控制流。
每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。
8.2.2并发流
一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。
多个流并发地执行的一般现象称为并发。一个进程和其他进程轮流运行的概念称为多任务. 一个进程执行它的控制流的一部分的每一时间段叫做时间片 。因此,多任务也叫做时间分片 。果两个流并发地运行在不同的处理器核或者计算机上,那么我们称它为并行流,它们并行地运行且执行。
8.2.3私有地址空间
一个进程为每个程序提供它自己的私有地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读或者写的。
地址空间顶部是保留给内核的。地址空间的这个部分包含内核在代表进程执行指令时使用的代码、数据和栈。
8.2.4用户模式和内核模式
没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。因此,用户程序必须通过系统调用接口间接地访问内核代码和数据。
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序码时,处理器就把模式从内核模式改回到用户模式。
Linux 提供了一种聪明的机制,叫做/proc 文件系统,它允许用户模式进程访问内核数据结构的内容。/proc 文件系统将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。比如,你可以使用/proc 文件系统找出一般的系统属性
8.2.5上下文切换
上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成。
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定就叫做调度( schedule ) ,是由内核中称为调度器( scheduler )的代码处理的。当内核选择一个新的进程运行时,我们就说内核调皮了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为土下丈切换的机制来将控制转移到新的进程,上下
文切换1)保存当前进程的上下文, 2) 恢复某个先前被抢占的进程被保存的上下文, 3) 将控
制传递给这个新恢复的进程。
当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为某个等待时间发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。中断也可以发生上下文切换。
8.4进程控制
8.4.1获取进程ID
每个进程都有一个唯一的正数进程ID(PID)。getpid函数返回调用进程的PID。getppid函数返回它的父进程的PID。
8.4.2创建和终止进程
进程总是处于下面三种状态之一:
①运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
②停止。进程的执行被挂起,且不会被调度。当收到SIGSTOP\SIGTSTP
\SIDTTIN或者SIGTTOU信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行。
③终止。进程永远的停止了。进程会因为三种原因停止:1.收到一个信号,该信号的默认行为是终止进程。2.从主程序返回3.调用EXIT函数
新创建的子进程几乎但不完全与父进程相同。当父进程调用FORK时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
FORK函数只被调用一次,却会返回两次:一次是在调用父进程中,一次是在新创建的子进程中。在父进程中,FORK返回子进程的PID。在子进程中,FORK返回0.因为子进程的PID总是非零的,返回值就提供一个明确的方法来分辨程序是在父进程还是子进程中执行的。
例子:
8.4.3回收子进程
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。
当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个终止了但还未被回收的进程称为僵死进程。
一个进程可以通过调用WAITPID函数来等待它的子进程终止或者停止。
1.判断等待集合的成员
等待集合的成员是由参数PID来确定的:
①如果PID>0,那么等待集合就是一个单独的子进程,它的进程ID等于PID。
②如果PID=—1,那么等待集合就是由父进程所有的子进程组成的。
2.修改默认行为
可以通过将optioins 设置为常量WNOHANG 和WUNTRACED 的各种组合,修改默认行为:
.WNOHANG: 如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。
• WUNTRACED :挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回的PID 为导致返回的己终止或被停止子进程的PID。默认的行为是只返回己终止的子进程。当你想要检查已终止和被停止的子进程时,这个选项会有用。
.WNOHANG阳UNTRACED: 立即返回,如果等待集合中没有任何子进程被停止或已终止,那么返回值为0 ,或者返回值等于那个被停止或者己终止的子进程的PID 。
3.检查已回收子进程的退出状态
4.错误条件
如果调用进程没有子进程,那么WAITPID返回-1,并且设置ERRNO为ECHILD。如果WAITPID函数被一个信号中断,那么它返回-1.并设置ERRNO为EINTR.
5.wait函数
8.4.4让进程休眠
sleep函数将一个进程挂起一段8-56111717.png)
指定的时间。
如果请求的时间量已经到了,以上函数返回0,否则返回还剩下的要休眠的秒数。
PAUSE函数:让调用函数休眠,直到该进程收到一个信号
8.4.5加载并运行程序
execve函数在当前进程的上下文中加载并运行一个新程序
参数列表由下图表示.ARGV变量指向一个以NULL结尾的指针数组,其中每个指针都指向一个参数串。ARGV[0]是可执行目标文件的名字。
ENVP变量指向一个以NULL结尾的指针数组,其中每个指针指向一个环境变量串。
函数的主函数如下:
int main(int argc,char **argv,char **envp)
等价于其数组形式
UNIX提供了几个函数来操作环境数组:
getenv函数在环境数组中搜索字符串"name = value".如果找到了,返回一个指向VALUE的指针,否则返回NULL。
补充:外壳是一个交互型的应用级程序,它代表用户运行其他程序。最早的外壳是SH程序,后面更加丰富。外壳执行一系列的读、求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户执行程序。
数组指针、指针数组、函数指针、指针函数的区别
例如:
int *p[4]; //指针数组。 是个有4个元素的数组, 每个元素的是指向整型的指针。(数组的每个元素都是指针)
int (*p)[4]; //数组指针。 它是一个指针,指向有4个整型元素的数组。 (一个指针指向有4个整型元素的数组)
int func(void); //指针函数。 无参函数, 返回整型指针。 (函数的返回值为int)
int (*func)(void); //表示函数指针,可以指向无参, 且返回值为整型指针的函数。 (函数的返回值为int)
8.5信号
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。
在外壳命令上输入man 7 signal得到这30中不同类型的信号
8.5.1信号术语
传送一个信号到目的进程是由两个不同的步骤组成的:
①发送信号。发送一个信号给目的进程。发送信号可以有如下原因:1)内核检测到一个系统事件,比如被零除错误或者子进程终结。2)一个人进程调用KILL函数,显示地要求一个信号给目的进程
②接收信号。当目的进程被内核强迫以某种方式对信号的发送作出反应时,目的进程就接受了信号。终止或者通过执行一个称谓信号处理程序的用户层函数捕获这个信号。
如果一个进程有一个类型为K的待处理信号,那么任何接下来发送到这个进程的类型为K的信号都不会排队等待,它们只是被简单地丢弃。
8.5.2发送信号
1.进程组
进程组是由一个正整数进程组ID来标识的。GETPGRP函数返回当前进程的进程组ID:
setgpid函数将进程PID的进程组改为PGID。如果PID是0,那么就使用当前进程的PID。
2.从键盘发送信号
UNIX外壳使用作业这个抽象概念来表示为对一个命令行求值而创建的进程。创建一个由两个进程组成的前台作业,这两个进程是通过UNIX管道连接起来的:一个进程运行LS程序,另一个运行SORT程序。
在键盘上输入ctrl-c会导致一个SIGINT信号被发送到外壳。
3.用KILL函数发送信号
进程通过调用KILL函数发送信号给其他进程
如果PID大于0,那么KILL函数发送信号SIG给进程PID。如果PID小于0,那么KILL发送信号给SIG给进程组ABS中的每个进程。
用ALARM函数发送信号
进程可以通过调用ALARM函数向它自己发送SIGALRM
8.5.3接收信号
当内核从一个异常处理程序返回,准备将控制传递给进程p 时,它会检查进程p 的未被阻塞的待处理信号的集合(pending&-blocked) 。如果这个集合为空(通常情况下),那么内核将控制传递到p 的逻辑控制流中的下一条指令
然而,如果集合是非空的,那么内核选择集合中的某个信号k ( 通常是最小的k) , 并且强制p 接收信号k. 收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回p 的逻辑控制流中的下一条指令(]next) 。每个信号类型都有一个预定义的默认行为,是下面中的一种:
·进程终止。
·进程终止并转储存储器(dump core) 。
.进程停止直到被SIGCONT 信号重启.
.进程忽略该信号。
signal 函数可以通过下列三种方法之一来改变和信号signum 相关联的行为:
·如果handler 是SIG_IGN. 那么忽略类型为signum 的信号。
·如果handler 是SIG_DFL,那么类型为signum 的信号行为恢复为默认行为。
·否则, handler 就是用户定义的函数的地址,这个函数称为信号处理程序(signalhandler) ,只要进程接收到一个类型为signurn 的信号,就会调用这个程序。通过把处理程序的地址传递到signal 函数从而改变默认行为,这叫做设置信号处理程序( installing the handler) 。调用信号处理程序称为捕获信号。执行信号处理程序称为处理信号。
8.5.4信号处理问题
对于只捕获一个信号并终止的程序来说,信号处理是简单直接的。然而,当一个程序要捕获多个信号时,一些细微的问题就产生了。
·待处理信号被阻塞。Unix 信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。比如,假设一个进程捕获了一个SIGINT 信号,并且当前正在运行它的SIGINT 处理程序。如果另一个SIGINT 信号传递到这个进程,那么这个SIGINT 将变成待处理的,但是不会被接收,直到处理程序返回。
·待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为k 的信号传送到一个目的进程,而由于目的进程当前正在执行信号k 的处理程序,所以信号k 是阻塞的,那么第二个信号就被简单地丢弃,它不会排队等待。关键思想是存在一个待处理的信号仅仅表明至少已经有一个信号到达了。
·系统调用可以被中断。像read 、wait 和accept 这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误条件,并将errno。设置为EINTR。
运行结果:linux> ./signa11
Hello from child 10320
Hello from child 10321
Hello from child 10322
Ha且dler reaped chi1d 10320
Ha且dler reaped child 10322
processing input
8.5.5可移植的信号处理
不同系统之间,信号处理语义的差异〈比如一个被中断的慢速系统调用是重启还是永久放弃)是Unix 信号处理的一个缺陷。为了处理这个问题, Posix 标准定义了sigaction 函数,它允许像Linux 和Solaris 这样与Posix 兼容的系统上的用户,明确地指定他们想要的信号处理语义。
sigaction 函数运用并不广泛,因为它要求用户设置多个结构条目。一个要简洁的方式,最初是由W.Richard Stevens 提出的[109] ,就是定义一个包装函数,称为Signal ,它调用sigaction 。Signal 的定义,它的调用方式与signal 函数的调用方式一样。Signal 包装函数设置了一个信号处理程序,其信号处理语义如下:
·只有这个处理程序当前正在处理的那种类型的信号被阻塞。
·和所有信号实现一样,信号不会排队等待。
·只要可能,被中断的系统调用会自动重启。
·一旦设置了信号处理程序,它就会一直保持,直到Signal 带着handler 参数为SIG_IGN 或者SIG.:_DFL 被调用。(一些比较老的'Unix 系统会在一个处理程序处理完一个信号之后,将信号行为恢复为它的默认行为。
8.5.6显式地阻塞和取消阻塞信号
应用程序可以使用sigprocmask 函数显式地阻塞和取消阻塞选择的信号:
sigprocmask 函数改变当前已阻塞信号的集合(8.5.1 节中描述的blocked 位向量〉。具体的行为依赖于how 的值:
• SIG_BLOCK: 添加set 中的信号到blocked 中(blocked = blocked I set).
• SIg_.UNBLOCK: 从blocked 中删除set 中的信号(blocked = blocked & -set) 。
• SIG_SEtMASK : blocked = set 。
如果。ldset 非空, blocked 位向量以前的值会保存在oldset 中。
可以使用下列函数操作像set 这样的信号集合。sigemptyset 初始化set 为空集。
sigfillset 函数将每个信号添加到set 中。sigaddset 函数添加signum 到set , sigdelset从set 中删除signum,如果signum 是set 的成员,那么sigismember 返回1 ,否则返回0。
8.5.7同步流以避免讨厌的并发错误
当父进程创建一个新的子进程时,它就把这个子进程添加到作业列表中。当父进程在SIGCHLD 处理程序中回收一个终止的〈僵死〉子进程时,它就从作业列表中删除这个子进程。乍一看,这段代码看上去是对的。不幸的是,可能发生下面的情况:
1)父进程执行fork 函数,内核调度新创建的子进程运行,而不是父进程。
-
在父进程能够再次运行之前,子进程就终止,并且变成一个僵死进程,使得内核传递一个SIGCHLD 信号给父进程。
-
后来,当父进程再次变成可运行但又在它执行之前,内核注意到待处理的SIGCHLD 信号,并通过在父进程中运行处理程序接收这个信号
4)处理程序回收终止的子进程,并调用deletejob ,这个函数什么也不做,因为父进程还没有把该子进程添加到列表中。
5)在处理程序运行完毕后,内核运行父进程,父进程从fork 返回,通过调用addjob 错误地把(不存在的〉子进程添加到作业列表中。
8.6非本地跳转
非本地跳转(nonlocal jump) ,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。非本
地跳转是通过setjmp和longjmp 函数来提供的。
setjmp 函数只被调用一次,但返回多次:一次是当第一次调用setjmp ,而调用环境保存在缓冲区env 中时:一次是为每个相应的longjmp 调用。另一方面, longjmp 函数被调用一次,但从不返回。
参考资料
《深入理解计算机系统》
遇到的问题及解决
书本中有很多的代码举例加深对函数的理解,但是不是每个函数都能吃透,希望老师能够讲解。例如有的例子涉及并发编程就不是很理解,我也会查阅更多资料去解决这个问题