2017-2018-1 20155219 《信息安全系统设计基础》第6周学习总结

教材学习内容总结

异常

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

  • 异常处理程序完成后有三种情况

1)处理程序将控制返回给事件发生时正在执行的当前指令

2)处理程序将控制返回给没有发生异常将会执行的下一条指令

3)处理程序终止被中断的程序

异常处理

(1)异常表与异常号

异常表:当处理器检测到有事件发生时,它会通过跳转表,进行一个间接过程调用(异常),到异常处理程序。系统启动时操作系统分配和初始化一张异常表。

异常号:系统中可能的某种类型的异常都分配了一个唯一的非负整数的异常号。异常号是到异常表中的索引,异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里。

(2)异常与过程调用

异常类似于过程调用,但是有一些重要的不同:

处理器压入栈的返回地址,是当前指令地址或者下一条指令地址。
处理器也把一些额外的处理器状态压到栈里
如果控制一个用户程序到内核,所有项目都压到内核栈里。
异常处理程序运行在内核模式下,对所有的系统资源都有完全的访问权限。

异常的类别

异常的分类:中断、陷阱、故障和终止。

中断:异步发生,是来自处理器外部的I/O设备的信号的结果。 硬件异常中断处理程序通常称为中断处理程序。

陷阱和系统调用:
陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
普通的函数运行在用户模式中,用户模式限制了函数可以执行的指令的类型,而且它们只能访问与调用函数相同的栈。系统调用运行在内核模式中,内核模式允许系统调用执行指令,并访问定义在内核中的栈。

故障:是由错误情况引起的。
终止:是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序从不将控制返回给应用程序。
031号:由intel架构师定义的异常;32255号:操作系统定义的中断和陷阱。
每一个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。
如下图:image

简单概述

控制转移:从一条指令到下一条指令的过渡。

控制流:控制转移序列。

最简单的控制流:平滑的序列,每条指令在存储器中都是相邻的。

平滑流的突变:由于跳转、调用和返回等指令造成两条指令不相邻。

异常控制流(ECF):现代操作系统通过使控制流发生突变来对系统状态做出反应,这些突变称为异常控制流。

异常控制流发生在计算机系统的各个层次

硬件层:硬件检测到的事件会触发控制突然装移到异常处理程序
操作系统层:内核通过上下文转换将控制从一个用户进程转移到另一个用户进程。
应用层:一个进程可以发送信号到到另一个进程,而接收者将会控制突然转移到它的一个信号处理程序。
一个程序可以通过回避通常的栈规则,并执行到其它函数中任意位置的非本地跳转来对错误做出反应。
ECF是操作系统用来实现I/O、进程和虚拟存器的基本机制。

应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务。

操作系统为应用程序提供了强大的ECF机制,用来创建新进程、等待进程终止、通知其他进程中系统的异常事件、检测和相应这些事件。

ECF是计算机系统中实现并发的基本机制。

软件异常允许程序进行非本地跳转来响应错误情况。

非本地跳转是一种应用层ECF,在C中通过setjump和longjmp提供。

用户模式和内核模式

模式位:用某个控制寄存器中的一个位模式,限制一个应用可以执行的指令以及它可以访问的地址空间范围。

当设置了位模式,进程就运行在内核模式中,一个运行在内核模式中的进程可以中兴指令集中的任何指令,而且可以访问系统中任何存储器位置。

没有设置位模式时,进程就运行在用户模式中,不允许执行特权指令,例如停止处理器、改变位模式,或者发起一个I/O操作。

用户程序必须通过系统调用接口间接的当问内核代码和数据。

进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障、或者陷入系统调用这样的异常。

上下文切换

1、上下文就是内核重新启动一个被抢占的进程所需的状态。
2、调度:内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。有内核中称为调度器的代码处理的。
3、上下文切换机制:

(1)保存当前进程的上下文
(2)恢复某个先前被抢占的进程被保存的上下文
(3)将控制传递给这个新恢复的进程
4、引起上下文切换的情况

(1)当内核代表用户执行系统调用时
(2)中断时

创建和终止进程

1、进程总处于三种状态

(1)运行:进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。

(2)停止:程序的执行被挂起,,且不会被调度。

(3)终止:进程用永远停止了。终止原因:
- 收到一个信号,默认行为是终止进程;
- 从主进程返回
- 调用exit函数。

2、父进程通过调用fork函数创建一个新的运行的子进程。

3、子进程和父进程的异同:

异:有不同的PID
同:用户级虚拟地址空间,包括:文本、数据和bss段、堆以及用户栈。任何打开文件描述符,子进程可以读写父进程中打开的任何文件。

4、fork函数: 因为父进程的PID总是非零的,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
fork函数的特点:
(1)调用一次,返回两次

(2)并发执行

(3)相同的但是独立的地址空间

(4)共享文件

系统调用错误处理

系统会使用错误处理包装函数,系统级函数是小写,他们的包装函数名大写,包装函数调用基本函数,有任何问题就终止,如果没有问题和基本函数是一样的。

获取进程ID

每个进程都有一个唯一的正数进程ID(PID)。

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回调用进程的PID
pid_t getppid(void);    返回父进程的PID(创建调用进程的进程)

终止进程

用exit函数。

#include <stdlib.h>
void exit(int status);
exit函数以status退出状态来终止进程。

修改默认行为——options

image

加载并运行程序——execve函数

execve函数:在当前进程的上下文中加载并运行一个新程序。

#include <unistd.h>

    int execve(const char *filename,const char *argv[],const char *envp[]);
    //返回:若成功,则不返回,若错误,返回-1

特点:execve调用一次从不返回

image

新程序开始时:

image

  • fork函数与exec函数的不同之处:

fork函数是创建新的子进程,是父进程的复制体,在新的子进程中运行相同的程序,父进程和子进程有相同的文件表,但是不同的PID

execve函数在当前进程的上下文中加载并运行一个新的程序,会覆盖当前进程的地址空间,但是没有创建一个新进程,有相同的PID,继承文件描述符。

信号

信号生命周期
:信号产生、信号注册、信号注销、信号处理

其中信号产生的四中类型:

  • 硬件产生-除零错误
  • 进程产生-kill指令
  • 内核产生-闹钟超时

信号处理-三种方法:

  • 捕捉:signal函数

  • 忽略信号:SIG_IGN

  • 默认操作:SIG_DFL

发送信号的两个不同步骤:
(1)发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。

发送信号的两个原因:
(1)内核监测到一个系统事件,比如被零除错误或者子进程终止。

(2)一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。

(2)接收信号:信号处理程序捕获信号的基本思想。

2、待处理信号:一个只发出而没有被接收的信号

一个进程可以有选择性地阻塞接收某种信号。
待处理信号不会被接收,直到进程取消对这种信号的阻塞。

一个待处理信号最多只能被接受一次,pending位向量:维护着待处理信号集合,blocked向量:维护着被阻塞的信号集合。

待处理信号不会排队等待;

多信号处理方法:

  • 递归,调用同一个处理函数

  • 忽略第二个信号

  • 阻塞第二个信号直至第一个处理完毕

非本地跳转

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

longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。

setjump函数和longjmp函数的区别:

  • setjmp函数只被调用一次,但返回多次;

  • longjmp函数被调用一次,但从不返回。

教材学习中的问题和解决过程

  • 问题1:关于如下代码
#include "csapp.h"
int main() 
{
    pid_t pid;
    int x = 1;
    pid = Fork(); //line:ecf:forkreturn
    if (pid == 0) {  /* Child */
    printf("child : x=%d\n", ++x); //line:ecf:childprint
    exit(0);
    }
    /* Parent */
    printf("parent: x=%d\n", --x); //line:ecf:parentprint
    exit(0);
}
  • 问题1学到的知识:

fork函数调用一次,返回两次

fork函数并发执行,内核能够以任何方式交替执行它们的逻辑控制流中的指令
相同和不同:

相同:用户栈、本地变量值、堆、全局变量值、代码

不同:私有地址空间

共享文件是什么?:子进程继承了父进程所有的打开文件。参考10.6节笔记。

如果调用了fork函数n次,产生2的n次方个进程。

代码调试中的问题和解决过程

exec1.c

#include <stdio.h>
#include <unistd.h>

int main()
{
    char    *arglist[3];

    arglist[0] = "ls";
    arglist[1] = "-l";
    arglist[2] = 0 ;//NULL
    printf("* * * About to exec ls -l\n");
    execvp( "ls" , arglist );//第一个参数传递的是文件名
    printf("* * * ls is done. bye");

    return 0;
}

运行结果如下图:

  • execvp()函数

  • 会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。

  • 返回值:
    如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。

  • exevp函数调用成功没有返回,所以没有打印出“* * * ls is done. bye”这句话

exec2.c

exec2与exec1的区别就在于exevp函数的第一个参数,exec1传的是ls,exec2直接用的arglist[0],不过由定义可得这两个等价,所以运行结果是相同的

exec3.c

函数中execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……最后一个参数必须用空指针(NULL)作结束

查看帮助文档image
得到:execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。

函数原型如下:int execlp(const char * file,const char * arg,....);

故exec3.c中函数execlp("ls", "ls", "-l", NULL);的意思是从环境变量中找到“ls”文件并执行ls -l指令。

故本例指定了环境变量,然后依然执行了ls -l指令,成功后没有返回,所以最后一句话不会输出。运行结果同exec1。

exec函数族与fork函数的联合使用

fork()函数通过系统调用创建一个与原来进程(父进程)几乎完全相同的进程,在fork后的子进程中使用exec函数族,可以装入和运行其它程序(子进程替换原有进程,和父进程做不同的事),fork创建一个新的进程就产生了一个新的PID,exec启动一个新程序,替换原有的进程,因此这个新的被exec执行的进程的PID不会改变。
exec函数族装入并运行程序path/file,并将参数arg0(arg1, arg2, argv[], envp[])传递给子程序,出错返回-1。

forkdemo1.c

#include    <stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(){
    int ret_from_fork, mypid;
    mypid = getpid();              
    printf("Before: my pid is %d\n", mypid);
    ret_from_fork = fork();
    sleep(1);
    printf("After: my pid is %d, fork() said %d\n",
            getpid(), ret_from_fork);
    return 0;
}

运行结果
image
这个代码先是打印进程pid,然后调用fork函数生成子进程,休眠一秒后再次打印进程id,这时父进程打印子进程pid,子进程返回0。
父进程通过调用fork函数创建一个新的运行子进程。
调用一次,返回两次。一次返回到父进程,一次返回到新创建的子进程。

forkdemo2.c

运行结果image
这个代码调用两次fork,一共产生四个子进程,所以会打印四个aftre输出。

forkdemo3.c

运行结果image
发现printf语句重复,并且if ( fork_rv == 0 )得到的应该是子进程而不是父进程,进行修改后得到新的代码如下

#include    <stdio.h>
#include    <stdlib.h>
#include    <unistd.h>
int fork_rv;

int main()
{

    printf("Before: my pid is %d\n", getpid());

    fork_rv = fork();       /* create new process   */

    if ( fork_rv == -1 )        /* check for error  */
        perror("fork");
    else if ( fork_rv == 0 ){ 
        printf("I am the child. my pid is %d\n", getpid());

        exit(0);
    }
    else{
        printf("I am the parent. my child is %d\n", fork_rv);
        exit(0);
    }

    return 0;
}

得到正确结果,并且这个代码进行了错误处理,提高了代码的健壮性。

forkdemo4.c

代码如下

#include    <stdio.h>
#include    <stdlib.h>
#include    <unistd.h>
int main(){
    int fork_rv;
    printf("Before: my pid is %d\n", getpid());
    fork_rv = fork();       /* create new process   */
    if ( fork_rv == -1 )        /* check for error  */
        perror("fork");
    else if ( fork_rv == 0 ){ 
        printf("I am the child.  my pid=%d\n", getpid());
        printf("parent pid= %d, my pid=%d\n", getppid(), getpid());
        exit(0);
    }
    else{
        printf("I am the parent. my child is %d\n", fork_rv);
        sleep(10);
        exit(0);
    }
    return 0;
}

运行结果:image
先打印进程pid,然后fork创建子进程,父进程返回子进程pid,所以输出parent一句,执行sleep(10)语句,休眠十秒。

forkgdb.c

运行结果如下:
image
父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。可以知道程序运行速度是极快的虽然parent运行两句后休眠,child运行一句后休眠,但每运行一段时间仍会有1秒停顿,是两个一起停顿,并不是我自己一开始想到的进程之间互相抢夺处理器。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰。

psh1.c

运行结果为:
image
image
其中:char * makestring( char *buf )函数是将你逐个输入的命令单词汇总成一个命令行。

int execute( char *arglist[] )函数命令指定的程序代码覆盖了shell程序代码,并在命令结束之后退出,shell就不能再接受新的命令。其参数是一个字符串,一个字符串构造参数列表argist,最后在数组末尾加上NULL

这个代码的具体用处是:
依次你输入要执行的指令与参数,回车表示输入结束,然后输入的每个参数对应到函数中,再调用对应的指令。

waitdemo1.c

代码如下

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

#define DELAY   4

void child_code(int delay)
{
    printf("child %d here. will sleep for %d seconds\n", getpid(), delay);
    sleep(delay);
    printf("child done. about to exit\n");
    exit(17);
}

void parent_code(int childpid)
{
    int wait_rv=0;      /* return value from wait() */
    wait_rv = wait(NULL);
    printf("done waiting for %d. Wait returned: %d\n", 
            childpid, wait_rv);
}
int main()
{
    int  newpid;
    printf("before: mypid is %d\n", getpid());
    if ( (newpid = fork()) == -1 )
        perror("fork");
    else if ( newpid == 0 )
        child_code(DELAY);
    else
        parent_code(newpid);

    return 0;
}

运行结果如下:
image
wait函数等到所有子程序都执行完之后才能执行

printf("done waiting for %d. Wait returned: %d\n", 
            childpid, wait_rv);

如果有子进程,则终止子进程,成功返回子进程pid。

waitdemo2.c

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

#define DELAY   10

void child_code(int delay)
{
    printf("child %d here. will sleep for %d seconds\n", getpid(), delay);
    sleep(delay);
    printf("child done. about to exit\n");
    exit(27);
}

void parent_code(int childpid)
{
    int wait_rv;    
    int child_status;
    int high_8, low_7, bit_7;

    wait_rv = wait(&child_status);
    printf("done waiting for %d. Wait returned: %d\n", childpid, wait_rv);

    high_8 = child_status >> 8;     /* 1111 1111 0000 0000 */
    low_7  = child_status & 0x7F;   /* 0000 0000 0111 1111 */
    bit_7  = child_status & 0x80;   /* 0000 0000 1000 0000 */
    printf("status: exit=%d, sig=%d, core=%d\n", high_8, low_7, bit_7);
}

int main()
{
    int  newpid;

    printf("before: mypid is %d\n", getpid());

    if ( (newpid = fork()) == -1 )
        perror("fork");
    else if ( newpid == 0 )
        child_code(DELAY);
    else
        parent_code(newpid);
}

运行结果如下image

此程序多了一个子进程的状态区分,把状态拆分成三块:exit,sig和core。更方便来区分程序此刻的状态。

代码托管链接在此

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 200/200 2/2 20/20 了解计算机系统、静态链接与动态链接
第三周 300/500 2/4 18/38 深入学习计算机算术运算的特性
第四周 500/1000 3/7 22/60 掌握程序崩溃处理、Linux系统编程等知识,利用所学知识优化myod,并实现head和tail命令
第五周 300/1300 2/9 10/70 掌握“进程”的概念,并学习应用相关函数;了解程序如何在机器上表示
第六周 169/1469 2/9 14/84 学习了关于异常控制的函数与用法

尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。

参考:软件工程软件的估计为什么这么难软件工程 估计方法

  • 计划学习时间:10小时

  • 实际学习时间:14小时

  • 改进情况:

(有空多看看现代软件工程 课件
软件工程师能力自我评价表
)

参考资料