20145219 《信息安全系统设计基础》第11周学习总结

20145219 《信息安全系统设计基础》第11周学习总结

教材学习内容总结

代码部分

argv

  • argv.h

    阅读头文件argv.h

    int makeargv(const char *s, const char *delimiters, char ***argvp);函数的功能是把命令行字符串转化为以NULL结尾的参数数组

    参数:

      s为命令行字符串
      delimiters为分割符
      argvp为指向参数数组的指针,转化成功返回标记的个数,错误返回-1并设置errno(记录系统最后一次错误的函数)
    
  • freemakeargv.c、makeargv.c、argtest.c

    可以使用gcc freemakeargv.c、makeargv.c、argtest.c -o argteat命令同时编译这三个c文件。

    只有当输入命令的个数等于2时,才能显示命令正确的结果,运行格式为:./可执行文件argtest 命令

env

  • environ.c

    代码中涉及函数:

    • getenv函数

    获得环境变量值的函数,参数是环境变量名name(如”HOME”、”PATH”)。如果环境变量存在,函数会返回环境变量值,即value的首地址;如果环境变量不存在,函数返回NULL

    • setenv函数

    修改或添加环境变量的函数

    1.如果name在环境中不存在,在环境中添加这个新的变量。

    setenv函数必须在environment list中增加一个新的entry,然后动态申请存储空间来存储name=value,并且使entry指向该空间。

    2.如果在环境中name已经存在,

      (a)若overwrite非0,那么更新name的value(实质是更新环境表,指向新的value)
      (b)若overwrite为0,则环境变量name不变,并且不出错
    

    setenv函数不必在environment list中增加一个新的entry。当overwrite为0, 则不必改动entry的指向;当overwrite非0, 则直接使该entry指向name=value,当然该name=value也是存储在动态申请的内存里。

  • environvar.c

    功能:简单打印环境变量表

全局变量environ则包含了该指针数组的地址

每个程序都有一个环境表,它是一个字符指针数组,其中每个指针包含一个以NULL结尾的C字符串的地址

fifo

FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。

FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

FIFO往往都是多个写进程,一个读进程。

  • consumer.c

    管道写端

  • producer.c

    管道读端

  • testmf.c

    调用mkfifo函数mkfifo(FIFO_NAME, 0777);

    依据FIFO_NAME创建fifo文件,0777依次是相应权限

    mkfifo()建立的FIFO文件其他进程都可以用读写一般文件的方式存取

pipe

创建管道并将其两端连接到两个文件描述符

array[0]为读数据端的文件描述符
array[1]为写数据端的文件描述符

内部则隐藏在内核中,进程只能看到两个文件描述符

  • listargs.c

    证明shell不重定向标记和文件名传递给程序

  • pipe.c

    引入oops,当linux系统执行代码遇到问题时,就会报告oops

  • pipedemo.c

    创建管道并使用管道来向自己发送数据

  • pipedemo2.c

    使用管道向自己发送数据

    将pipe和fork结合起来,创建一对通过管道来通信的进程。

    在程序中显示了从键盘到进程,从进程到管道,再从管道到进程以及从进程回到终端的数据传输流

  • stdinredir1.c

    close-then-open模型:将stdin定向到文件,程序中先关闭标准输入流,后打开文件,进行重定向

  • stdinredir2.c

    open..dup2..close模型:将dup2(fd,0)将close(0),dup(fd)合在一起,实现重定向

  • testtty.c

    write(int handle,void *buf,int nbyte);

      handle是文件描述符
      *buf是指向一端内存单元的指针
      nbyte是要写入指定文件的字节个数,成功时返回字节个数,否则返回-1。
    
  • whotofile.c

    重定向到文件

signal

  • sigdemo1.c

    连续输出五个hello,间隔时间为2秒

    输入的Ctrl+C停止程序会变成输出OUCH!,并不能停止程序。

  • sigdemo2.c

    一直输出haha

    按Ctrl+C不能停止

    参数:

      SIG_DFL,SIG_IGN 分别表示无返回值的函数指针,指针值分别是0和1,这两个指针值逻辑上讲是实际程序中不可能出现的函数地址值。
      SIG_DFL:默认信号处理程序
      SIG_IGN:忽略信号的处理程序
    
  • sigdemo3.c

    输入什么,就输出什么

    输入的Ctrl+C也无法终止程序,只有输入quit的时候才会退出

  • sigactdeom.c

    int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);函数依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号

    参数:

      SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
      SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
      SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
    
  • sigactdemo2.c

    休息seconds秒后返回,或者被信号中断且信号处理函数返回后sleep()返回0

    如果不计较返回值的话,pause()的功能相当于无限期的sleep()。

exec

  • exec1.c

    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)作结束

    成功后没有返回,所以最后一句话不会输出。运行结果同exec1。

forkdemo

  • forkdemo1.c

    先打印进程pid,然后调用fork函数生成子进程,休眠一秒后再次打印进程id,这时父进程打印子进程pid,子进程返回0

    调用一次:父进程通过调用fork函数创建一个新的运行子进程

    返回两次:一次返回到父进程,一次返回到新创建的子进程。

  • forkdemo2.c

    调用两次fork,一共产生四个子进程,会打印四个aftre输出。

    调用n次fork的话就会打印出2^n个after

  • forkdemo3.c

    fork产生子进程,父进程返回子进程pid,不为0,所以输出父进程的那句话,子进程返回0,所以会输出子进程那句话

  • forkdemo4.c

    先打印进程pid,然后fork创建子进程,父进程返回子进程pid,所以输出parent一句,休眠十秒;子进程返回0,所以输出child与之后一句。

forkgdb

父进程先打印两句,然后休眠一秒,然后打印一句;子进程先打印一句,然后休眠一秒,然后打印两句。

这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行

线程之间相互独立互不干扰

psh

  • psh1.c

    输入要执行的指令,回车表示输入结束,然后输入的每个参数对应到函数中,再调用对应的指令

    第一个参数是程序名,然后依次是程序参数。

    一个字符串一个字符串的构造参数列表argist,最后在数组末尾加上NULL

    将arglist[0]和arglist数组传给execvp。

    程序正常运行,execvp命令指定的程序代码覆盖了shell程序代码,并在命令结束之后退出,shell就不能再接受新的命令。

  • psh2.c

    psh2.c比起psh1.c来,多了循环判断,不退出的话就会一直要你输入指令

testbuf

  • testbuf1.c

    先输出hello,然后保持在循环中不结束进程。

  • testbuf2.c

    testbuf2.c中fflush(stdout)的效果和换行符\n是一样的

  • testbuf3.c

    将内容格式化输出到标准错误、输出流中

testpid

输出当前进程pid和当前进程的父进程的pid。

testpp

运行后出现段错误……

给pp分配空间后运行不再出错

testsystem

system()执行shell命令,也就是向dos发送一条指令。后面可以跟两个参数,然后向dos发送这两个命令,分别执行。

waitdemo

  • waitdemo1.c

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

  • waitdemo2.c

    waitdemo2.c比起waitdemo1.c来就是多了一个子进程的状态区分,把状态拆分成三块,exit,sig和core

课本部分:异常控制流

概述

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

  • 控制流:控制转移序列。

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

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

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

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

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

    ECF是操作系统用来实现I/O、进程和虚拟存器的基本机制。

    • 应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务。
    • 操作系统为应用程序提供了强大的ECF机制,用来创建新进程、等待进程终止、通知其他进程中系统的异常事件、检测和相应这些事件。

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

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

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

异常

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

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

      1)处理程序将控制返回给事件发生时正在执行的当前指令
      2)处理程序将控制返回给没有发生异常将会执行的下一条指令
      3)处理程序终止被中断的程序
    
  • 异常处理

    (1)异常表与异常号

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

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

    (2)异常与过程调用

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

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

    (1)中断

      来自处理器外部I/O设备的信号的结果
      异步发生
      总是返回到下一条指令
    

    (2)陷阱

      有意的异常
      同步发生
      最重要的用途是提供系统调用。系统调用运行在内核模式中,并且可以访问内核中的栈
      总是返回到下一条指令
    

    (3)故障

    故障指令:同步发生的,执行当前指令的结果。

      由错误状况引起
      可能能够被故障处理程序修正
      要么重新执行引起故障的指令,要么终止
      经典的故障:缺页异常。
    

    (4)终止

      不可恢复的致命错误造成的结果,通常是硬件错误。
      不会将控制返回给应用程序。
    
  • Linux/IA32系统中的异常

    一共有256种不同的异常类型。

    (1)故障和终止

    • 除法错误(浮点异常)
      • 异常0
      • 原因:除以零,或结果太大
      • 终止程序
    • 一般保护故障(段故障)
      • 异常13
      • 原因:程序引用了一个未定义的虚拟存储器区域,或因为程序试图写一个只读的文本段
      • 终止程序
    • 缺页
      • 异常14
      • 重新执行产生故障的指令
      • 返回当前地址
    • 机器检查
      • 异常18
      • 在导致故障的指令执行中检测到致命的硬件错误
      • 终止程序

    (2)系统调用

    每一个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量

    在IA32中,系统调用通过一条陷阱指令提供:int n(n为异常号)

    所有的到Linux系统调用的参数都是通过寄存器传递的

      %eax:包含系统调用号
      %ebx,%ecx,%edx,%esi,%edi,%ebp:包含最多六个任意参数
      %esp:栈指针,不能使用,因为当进入内核模式时,内核会覆盖它。
    

进程

  • 一个执行中的程序的实例

  • 系统中每个程序都是运行在某个进程上下文中的。

    上下文是由程序正确运行所需的状态组成的。

    状态包括存放在存储器中的程序代码和数据,栈、通用目的寄存器内容、程序计数器、环境变量和打开文件描述符的集合。

  • 进程提供给应用程序的关键抽象

      一个独立的逻辑控制流,提供一个假象:程序独占地使用处理器
      一个私有的地址空间,提供一个假象:程序独占地使用存储器系统
    
  • 逻辑控制流

    程序计数器(PC)值的序列叫做逻辑控制流,简称逻辑流。

    进程是轮流使用处理器的,每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。

  • 并发流

    一个逻辑流的执行在时间上与另一个流重叠(与是否在同一处理器无关),这两个流并发的运行。

      并发:多个流并发的执行
      多任务:一个进程和其他进程轮流运行(也叫时间分片)
      时间片:一个进程执行它的控制流的一部分的每一时间段
      并行:两个流并发的运行在不同的处理机核或者计算机上。
    

    并行流并行的运行,并行的执行。

  • 私有地址空间

    进程为程序提供的假象,好像它独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读写的。

  • 用户模式和内核模式

    用户模式和内核模式的区别就在于用户的权限上,权限指的是对系统资源使用的权限。

    (1)模式位

    处理器通常用某个控制寄存器中的一个模式位来描述进程当前享有的特权。

      设置模式位:内核模式(超级用户)
      没有设置模式位:用户模式
    

    (2)转换模式

    运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过如中断、故障或陷入系统调用之类的异常。

    (3)/proc文件系统

    将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。它允许用户模式进程访问内核数据结构的内容。

  • 上下文切换

    操作系统内核使用上下文切换这种较高层形式的异常控制流来实现多任务。

    上下文切换机制建立在较底层异常机制之上。

    (1)上下文

    内核重新启动一个被抢占的进程所需的状态。

    由通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、内核数据结构(页表、进程表、文件表)这些对象的值组成

    (2)上下文切换机制

    • 保存当前进程的上下文
    • 恢复某个先前被抢占的进程被保存的上下文
    • 将控制传递给这个新恢复的进程。

    (3)上下文切换原因

    • 内核代表用户执行系统调用时(进程休眠)
    • 中断

系统调用错误处理

  • 错误处理

    包装函数被封装在一个源文件(csapp.c)中,这个文件被编译和链接到每个程序中。一个独立的头文件(csapp.h)中包含这些包装函数的函数原型。

    (1)Unix系统中的错误处理

    • Unix风格的错误处理

    通常形式:

      if ((pid = wait(NULL))<0){
          fprintf(stderr,"wait error: %s\n",strerror(errno));
          exit(0);
      }
      //strerror函数返回某个errno值的文本描述
    
    • Posix风格的错误处理

    任何有用的结果都返回在通过引用传递进来的函数参数中

    通常形式:

      if ((retcode = pthread_create(&tid,NULL,threadd,NULL))!=0){
          fprintf(stderr,"pthread_create error: %s\n",strerror(retcode));
          exit(0);
      }
    
    • DNS风格的错误处理

    gethostbyname和gethostbyaddr函数失败时返回NULL指针,并设置全局变量h_errno。

    通常形式:

      if ((p = gethostbynae(name)) ==NULL)}
      fprintf(stderrr,"gethostbyname error: %s\n:",hstrerror(h_errno));
      exit(0);
      }
    
    • 错误报告函数

    (2)错误处理包装函数

    系统使用错误处理包装函数,系统级函数是小写,包装函数名大写。

    包装函数调用基本函数,有问题终止,如果没有问题和基本函数一致。

    • Unix风格的错误处理包装函数

        void Kill(pid_t pid,int signum)
        {
            int rc;
            if ((rc = kill(pid,signum))<0)
                unix_error("Kill error");
        }
      

      成功返回void,如果错误,打印一条消息然后退出。

    • Posix风格的错误处理包装函数

        void Pthread_detach(pthread_t tid){
            int rc;
            if ((rc = pthread_detach(tid)) != 0)
                posix_error(rc,"Pthread_detach error");
        }
      

      成功时返回void。

    • DNS风格的错误处理包装函数

        struct hostent *Gethostbyname(const char *name)
        {
            struct hostent *p;
        
            if ((p = gethostbyname(name)) == NULL)
                dns_error("Gethostbyname error");
            return p;
        }
      

进程控制

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

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

    (1)进程的三种状态

    • 运行
    • 停止:被挂起且不会被调度
    • 终止:永远停止。
      • 收到信号,默认行为为终止进程
      • 从主程序返回
      • 调用exit函数
        (2)创建进程

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

    fork函数定义如下:

      #include <sys/types.h>
      #include <unistd.h>
      
      pid_t fork(void);
    

    fork函数只被调用一次,但是会返回两次:父进程返回子进程的PID,子进程返回0.如果失败返回-1。

    (3)终止进程

    exit函数

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

    exit函数以status退出状态来终止进程。

  • 回收子进程

    进程终止后还要被父进程回收,否则处于僵死状态。

    如果父进程没有来得及回收,内核会安排init进程来回收他们。init进程的PID为1.

    一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。

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

    成功返回子进程PID,如果WNOHANG,返回0,其他错误返回-1。

    (1)判定等待集合的成员

      pid>0   等待集合是一个单独子进程,进程ID等于pid
      pid=-1  等待集合是由父进程所有的子进程组成
    

    (2)修改默认行为

    设置常量WNOHANG和WUNTRACED

    • WHOHANG

      • 默认:挂起调用进程
      • 修改:如果等待集合中的任何子程序都没有终止,立即返回
      • 返回值:0
    • WUNTRACED

      • 默认:返回已终止的子进程
      • 修改:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止
      • 返回值:导致返回的已终止或被终止子进程的PID
    • WNOHANG | WUNTRACED

      • 修改:立即返回
      • 返回值:0,或者返回值等于被停止或者已停止的子进程PID

    (3)检查已回收子进程的退出状态

    status参数:在wait.h头文件中定义了解释status参数的几个宏

      WIFEXITED:如果子进程通过调用exit或一个返回正常终止,就返回真
      WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态
      WIFSIGNALED:如果子进程是因为一个未被捕获的信号终止的,那么返回真
      WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSIGNALED返回为真时才定义这个状态
      WIFSTOPPED:如果引起返回的子进程当前是被停止的,那么返回真
      WSTOPSIG:返回引起子进程停止的信号的数量。只有在WIFSTOPPED返回为真时才定义这个状态
    

    (4)错误条件

    • 若调用进程没有子进程,waitpid返回-1,设置errno为ECHILD。
    • 若waitpid被一个信号中断,返回-1,设置errno为EINTR。
      (5)wait函数

    wait函数是waitpid函数的简单版本,wait(&status)等价于waitpid(-1,&status,0).

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

    成功返回子进程pid,出错返回-1

  • 让进程休眠

    (1)sleep函数

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

    sleep函数使一个进程挂起一段指定的时间。返回值是剩下还要休眠的秒数,时间结束返回0.

    (2)pause函数

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

    让调用函数休眠,直到该进程收到一个信号。

  • 加载并运行程序

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

    成功不返回,失败返回-1。

    • 操作环境数组的函数

      1)getnev函数

        #include <stdlib.h>
        
        char *getenv(const char *name);
      

      若存在则为指向name的指针,无匹配是null
      在环境数组中搜寻字符串"name=value",如果找到了就返回一个指向value的指针,否则返回null。
      2)setenv和unsetenv函数

        #include <stdlib.h>
        
        int setenv(const char *name, const char *newvalue, int overwrite);
      

      若成功返回0,错误返回-1

        void unsetenv(const char *name);//无返回值
      

      如果环境数组包含"name=oldvalue"的字符串,unsetenv会删除它,setenv会用newvalue代替oldvalue,只有在overwrite非零时成立。如果name不存在,setenv会将"name=newvalue"写进数组。

信号

unix信号是一种更高层次的软件形式的异常,它允许进程中断其他进程。

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

每种信号类型都对应某种系统事件。底层的硬件异常是由内核异常处理程序处理的。

  • 信号术语

    (1)传送一个信号到目的进程两个步骤:

    • 发送信号。

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

    发送信号的两种原因:
    内核检测到一个系统事件。
    一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程,一个进程可以发送信号给它自己。

    • 接收信号。

    当目的进程被内核强迫以某种方式的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序的用户层函数不活这个信号。

    (2)待处理信号

    一个只发出而没有被接收的信号叫做待处理信号。在任何时刻,一种类型至多只会有一个待处理信号。一个待处理信号最多只能被接收一次。

    一个进程可以有选择性地阻塞接收某种信号。当一种信号被阻塞时,他仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。

  • 发送信号

    (1)进程组

    每个进程都只属于一个进程组,进程组是由一个进程组ID来标识的。

    • 返回调用进程的进程组ID:getpgrp函数

        #include <unistd.h>
        
        pid_t getpgrp(void);
      

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

    • 改变自己或者其他进程的进程组:setpgid函数

        #include <unsitd.h>
        
        int setpgid(pid_t pid,pid_t pgid);
      

    成功返回0,错误返回-1

    (2)用/bin/kill程序发送信号

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

    (3)从键盘发送信号

    在任何时刻,至多只有一个前台作业和0个或多个后台作业。外壳为每个作业创建一个独立的进程组,一个作业对应一个进程组。

    (4)用kill函数发送信号:发送SIGKILL信号给它的子进程

    (5)用alarm函数发送信号:发送SOGALARM信号给它的子进程

  • 接收信号

    当内核从一个异常处理程序返回,准备将控制传递给进程P时,他会检查进程P的未被阻塞的处理信号的集合。如果这个集合为空,那么内核将控制传递到P的逻辑控制流中的下一条指令;如果集合是非空的,那么内核选择集合中的某个信号K(通常是最小的K0,并且强制P接收信号K。收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回P的逻辑控制流中的下一条指令。

    每个信号类型都有一个预定的默认行为:

    • 进程终止
    • 进程终止并转储存储器
    • 进程停止直到被SIGCONT型号重启
    • 进程忽略该信号

    signal函数

    可以通过下列三种方法之一来改变和信号signum相关联的行为:

    • 如果handler是SIG_IGN,那么忽略类型为signum的信号
    • 如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为
    • 否则,handler就是用户定义的函数的地址,这个函数成为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序,通过把处理程序的地址传递到signal函数从而改变默认行为,这叫做设置信号处理程序。

    进程可以通过使用signal函数来修改和信号相关的默认行为。唯一的例外是SIGSTOP和SIGKILL,它们的默认行为不能被修改。

  • 信号处理问题

    一个程序要捕获多个信号

    • 待处理信号被阻塞。Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。
    • 待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为K的信号传送到一个目的进程,而由于目的进程当前正在执行信号K的处理程序,所以信号K时阻塞的,那么第二和信号就简单地被简单的丢弃,不会排队等待。
    • 系统调用可以被中断。像read、wait和accept这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误的条件,并将errno设置为EINTR。

    不可以用信号来对其他进程中发生的事件计数。

  • 可移植的信号处理

    使用sigaction的一个包装函数,提供POSIX兼容系统上的可移植的信号处理。

非本地跳转

c语言提供的用户级异常控制流形式,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。
  • 实现函数:setjmp和longjmp
    #include <setjmp.h>

      int setjmp(jmp_buf env);
      int sigsetjmp(sigjmp_buf env, int savesigns);
      
      int longjmp(jmp_buf env,int retval);
      int siglongjmp(sigjmp_buf env, int retval);
    

    setjmp函数在env缓冲区中保存调用环境,以供longjmp使用,返回0。调用环境:程序计数器,栈指针,通用目的寄存器

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

操作进程的工具

STRACE:打印一个正在运行的程序和他的子程序调用的每个系统调用的痕迹
PS:列出当前系统中的进程(包括僵死进程)。
TOP:打印出关于当前进程资源使用的信息。
PMAP:显示进程的存储器映射。
/proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数据结构的内容,用户程序可以读取。

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

  • 问题1:运行sigdemo1.csigdemo2.csigdemo3.csigactdemo.csigactdemo2.c是按Ctrl+c不能终止程序进行。

  • 解决方法:sigdemo3.c中输入quit结束程序运行。其他的我直接关闭了终端.

  • 问题2:编译运行testpp.c后出现段错误……

  • 解决方法:参考上届学姐博客后发现,改变了代码,如下:

    阅读并理解代码后发现,错误原因是没给pp分配空间就调用了pp[0],声明的时候只是一个指针,而指针必须要初始化。

本周代码托管截图

  • 代码托管连接
  • 第1~4周项目分文件夹时弄乱了已删除,从第五周开始存在新项目里。
  • PS:git上学期用还好好的,这学期就总是会出问题,果然当初新建项目时很重要,新建的项目格式有问题之后调整就很费劲,总是会莫名其妙(其实是对git命令的含义不了解)的传不上去,或者全部覆盖(碰见问题按照百度解决时跟自己预期的结果不同)……还好我的代码一直留着,覆盖也只是重新传一次相同的,就是上传时间会改……

本周代码行数统计截图

其他(感悟、思考等,可选)

本周学习了异常控制流和老师上传到QQ群的一些代码,老实说,代码这部分我觉得比上一周工作量大了好多,理解到一半就产生了放弃的念头,不过后来想到,只是运行代码二不求甚解的话未免有些舍本逐末,于是及时制止了这种念头,阅读代码使我更深入的理解和分析了代码,理解了数组指针、指针数组、函数指针、指针函数的区别、了解了进程创建和控制的系统调用及函数使用。参考学姐学长们的博客还是给了我很大的帮助的,参考他们对于某些问题的解决方法也在一定程度上也帮助了我减少了困惑和不解,感谢学长学姐们。

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第零周 0/0 1/1 15/15 安装虚拟机
第一周 0/0 1/2 25/40 学习Linux命令
第二周 62/62 1/3 25/65 学习C编程
第三周 176/238 1/4 20/85 学习数的表示和计算
第五周 57/295 1/5 20/105 学习汇编语言,了解逆向思想
第六周 150/445 1/6 25/130 学习Y86处理器和HCL硬件描述语言
第七周 115/560 1/7 20/150 学习存储器相关知识
第八周 0/560 2/9 30/180 期中总结
第九周 186/746 2/11 20/200 Unix I/O及相关函数
第十周 423/1169 2/13 23/223 man、grep、cp、echostate、fileinfo、filesize、ls、setecho、spwd、testioctl、who代码的理解与调试
第十一周 1034/2203 2/15 31/254 学习异常控制流,并理解、运行老师给的代码

参考资料

posted @ 2016-11-27 23:07  20145219宋歌  阅读(321)  评论(2编辑  收藏  举报