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);
}
通过反汇编,可以看到与书上不同的是:
- 对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();
进程控制
- getpid返回调用进程的PID。
- getppid返回父进程的PID。
- exit终止进程。
- fork创建子进程。
- waitpid等待它的子进程终止或停止。
- sleep函数将一个进程挂起一段指定的时间。
- pause函数让调用函数休眠,直到该进程收到一个信号。
- 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);
}
从结果来看,与书上的结果一致。
练习题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。
子进程执行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。
使用waitpid函数不按照特定顺序回收僵死子进程:
使用waitpid函数按照创建子进程的顺序来回收僵死子进程:
execve函数调用一次从不返回。
在Unix shell和Web服务器这样的程序,大量使用fork和execve函数。
用户层的异常——信号
信号提供一种机制,通知用户进程发生了这些异常。
传送一个信号到目的进程是由两个不同步骤组成的:
- 发送信号
- 接受信号
注意:发出而没有接收的信号叫待处理信号,任何时刻,一种类型至多只会有一个待处理信号,一个待处理信号最多只能被接收一次。
发送信号的机制
- /bin/kill程序可以向另外的进程发送任意的信号。
- 从键盘发送信号。比如:在键盘上输入Ctrl+C会导致内核发送一个SIGINT信号,终止前台作业。Ctrl+Z会发送一个SIGTSTP信号,挂起前台作业。
- 调用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!这句话。
- 调用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);
}
linux提供阻塞信号的隐式和显式的机制:
- 隐式阻塞机制
- 显式阻塞机制
编写信号处理程序的规则:
- 安全的信号处理
- 处理程序要尽可能简单
- 在处理程序中只调用异步信号安全的函数
输出唯一安全的方法是使用write函数,printf或sprintf是不安全的。 - 保存和恢复errno
- 阻塞所有的信号
- 用volatile声明全局变量
- 用sig_atomic_t声明标志
- 正确的信号处理
书上通过一个例子:父进程通过SIGCHLD处理程序来回收子进程,而不是显示地等待子进程终止。
但实际运行中,发现出现了僵死进程,如图所示进程15031并没有被回收。
由此得到的教训是,不可用信号对其他进程中发生的事件计数。
修改程序,使得每次SIGCHLD处理程序被调用时,回收尽可能多的僵死子进程。
- 可移植的信号处理
sigaction函数,允许用户在设置信号处理时,明确指定他们想要的信号处理语义。
更简洁的包装函数signal。
消除竞争的方法:
- 用sigprocmask来同步进程。
一个进程的信号屏蔽字规定了当前阻塞而给该进程的信号集。调用函数sigprocmask可以检测或更改其信号屏蔽字,或者在一个步骤中同时执行这两个操作。
#include <signal.h>
int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset );
返回值:若成功则返回0,若出错则返回-1
- 用循环来等待信号。
- 用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返回。