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

我认为学得最差的一章是第8章异常控制流。因为这一章是老师上课讲的,课下我没有再去深入理解和实践,导致我在学习第十二章的过程中感觉很困难。“困难的事越做越容易,容易的事越做越困难”,那我现在重新好好学习一下这一章,希望能在今后的学习过程越来越轻松。

知识要点

Linux/86-64系统调用

C程序用syscall函数可以直接调用任何系统调用。

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

%rax包含系统调用号,%rdi,%rsi,%rdx……包含其他参数。

系统调用write版本的helloworld:

int main()
{
 write(1,"hello,world\n",13);
 _exit(0);
}

image

通过反汇编,可以看到与书上不同的是:

  • 对write函数的调用并不是用syscall,而是直接用call。
  • 但确实是通过寄存器传递的参数,len(13)放在%edx,string(.LC0)放在%esi,write第一个参数1放在%edi。
  • 在%eax里放的是0,不是write的系统调用号1,和_exit的系统调用号60也没有放入%eax,就直接用call调用了。

系统调用错误处理

当Unix系统级函数遇到错误时,通常会返回-1,并设置全局整数变量errno来表示出了什么错。所以我们应该注意检查错误,而不能偷懒。

可以使用错误处理包装函数。对于一个给定的函数foo,定义一个具有相同参数的包装函数Foo,包装函数再调用基本函数,检查错误。

例如Fork包装函数如下:

pid_t Fork(void)
{
    pid_t pid;
    if((pid = fork())<0)
      unix_error("Fork error");
    return pid;
}

那么,对fork的调用就可以直接写成:

pid = Fork();

进程控制

  1. getpid返回调用进程的PID。
  2. getppid返回父进程的PID。
  3. exit终止进程。
  4. fork创建子进程。
  5. waitpid等待它的子进程终止或停止。
  6. sleep函数将一个进程挂起一段指定的时间。
  7. pause函数让调用函数休眠,直到该进程收到一个信号。
  8. execve函数在当前进程的上下文中加载并运行一个新程序。

使用fork创建新进程:

#include "csapp.h"

int main() 
{
    pid_t pid;
    int x = 1;

    pid = Fork();
    if (pid == 0) {  /* child */
	printf("child : x=%d\n", ++x);
	exit(0);
    }

    /* parent */
    printf("parent: x=%d\n", --x);
    exit(0);
}

从结果来看,与书上的结果一致。

image

练习题8.2

#include "csapp.h"

int main() 
{
    int x = 1;

    if (Fork() == 0)
	   printf("printf1: x=%d\n", ++x);
    printf("printf2: x=%d\n", --x);
    exit(0);
}

首先对结果预测,p1是子进程x等于2,p2是父进程x等于0。

image

子进程执行p1、p2,父进程只执行p2。

练习题8.3

分析下面程序可能的输出序列。

#include "csapp.h"

int main() 
{
    if (Fork() == 0){
	printf("a");fflush(stdout);
    }
    else{
    printf("b");fflush(stdout);
    waitpid(-1,NULL,0);
    }
    printf("c");fflush(stdout);
    exit(0);
}

通过画进程图可知,有acbc、abcc、bacc三种可能。最终运行结果是bacc。

image

使用waitpid函数不按照特定顺序回收僵死子进程:

image

使用waitpid函数按照创建子进程的顺序来回收僵死子进程:

image

execve函数调用一次从不返回。

在Unix shell和Web服务器这样的程序,大量使用fork和execve函数。

image

用户层的异常——信号

信号提供一种机制,通知用户进程发生了这些异常。

传送一个信号到目的进程是由两个不同步骤组成的:

  1. 发送信号
  2. 接受信号

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

发送信号的机制

  1. /bin/kill程序可以向另外的进程发送任意的信号。
  2. 从键盘发送信号。比如:在键盘上输入Ctrl+C会导致内核发送一个SIGINT信号,终止前台作业。Ctrl+Z会发送一个SIGTSTP信号,挂起前台作业。
  3. 调用kill函数发送信号给其他进程(包括它们自己)

父进程用kill函数发送一个SIGKILL信号给它的子进程。

#include "csapp.h"

int main() 
{
    pid_t pid;

    if ((pid = Fork()) == 0) {   
	Pause();  
	printf("control should never reach here!\n");
	exit(0);
    }

    Kill(pid, SIGKILL);
    exit(0);
}

实验结果如下:

子进程被终止,没有打印control should never reach here!这句话。

image

  1. 调用alarm函数向它自己发送SIGALRM信号。

接收信号的机制

进程可以通过signal函数修改和信号相关联的默认行为。除SIGSTOP和SIGKILL的默认行为不能修改。

在书上的示例程序中,捕获了通过键盘发送的SIGINT信号,默认行为是立即终止该进程,这里修改为输出一条消息,终止该进程。

#include "csapp.h"

void handler(int sig) 
{
    printf("Caught SIGINT\n");
    exit(0);
}

int main() 
{
    if (signal(SIGINT, handler) == SIG_ERR) 
	unix_error("signal error");
    
    pause(); 
    
    exit(0);
}

image

linux提供阻塞信号的隐式和显式的机制:

  • 隐式阻塞机制
  • 显式阻塞机制

编写信号处理程序的规则:

  1. 安全的信号处理
  • 处理程序要尽可能简单
  • 在处理程序中只调用异步信号安全的函数
    输出唯一安全的方法是使用write函数,printf或sprintf是不安全的。
  • 保存和恢复errno
  • 阻塞所有的信号
  • 用volatile声明全局变量
  • 用sig_atomic_t声明标志
  1. 正确的信号处理

书上通过一个例子:父进程通过SIGCHLD处理程序来回收子进程,而不是显示地等待子进程终止。

但实际运行中,发现出现了僵死进程,如图所示进程15031并没有被回收。

image

由此得到的教训是,不可用信号对其他进程中发生的事件计数。

修改程序,使得每次SIGCHLD处理程序被调用时,回收尽可能多的僵死子进程。

image

  1. 可移植的信号处理

sigaction函数,允许用户在设置信号处理时,明确指定他们想要的信号处理语义。

更简洁的包装函数signal。

消除竞争的方法:

  1. 用sigprocmask来同步进程。

一个进程的信号屏蔽字规定了当前阻塞而给该进程的信号集。调用函数sigprocmask可以检测或更改其信号屏蔽字,或者在一个步骤中同时执行这两个操作。

#include <signal.h>
int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset );
返回值:若成功则返回0,若出错则返回-1
  1. 用循环来等待信号。
  2. 用sigsuspend来等待信号。

sigsuspend函数暂时用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号,其行为要么是运行一个处理程序,要么是终止该进程。如果它的行为是终止,那么该进程不从sigsuspend返回就直接终止。如果它的行为是运行一个处理程序,那么sigsuspend从处理程序返回,恢复调用sigsuspend是原有的阻塞集合。

#include <signal.h>

int sigsuspend(const sigset_t *mask);

C语言提供了一种用户级异常控制流形式——非本地跳转。通过两个函数提供:setjmp和longjmp。

通过下面这个示例程序,我们知道了这两个函数是如何工作的。

#include "csapp.h"

jmp_buf buf;

int error1 = 0; 
int error2 = 1;

void foo(void), bar(void);

int main() 
{
    int rc;

    rc = setjmp(buf);
    if (rc == 0)
	foo();
    else if (rc == 1) 
	printf("Detected an error1 condition in foo\n");
    else if (rc == 2) 
	printf("Detected an error2 condition in foo\n");
    else 
	printf("Unknown error condition in foo\n");
    exit(0);
}

void foo(void) 
{
    if (error1)
	longjmp(buf, 1); 
    bar();
}

void bar(void) 
{
    if (error2)
	longjmp(buf, 2); 
}

可见,首先是调用了setjmp保存当前环境,然后调用foo函数,foo再调用bar,如果遇到错误,就通过longjmp调用从setjmp返回。

image

posted on 2017-12-24 21:51  20155225江智宇  阅读(110)  评论(0编辑  收藏  举报