20145314郑凯杰《信息安全系统设计基础》第11周学习总结

20145314郑凯杰《信息安全系统设计基础》第11周学习总结

明确教材学习目标

  • 了解异常及其种类
  • 理解进程和并发的概念
  • 掌握进程创建和控制的系统调用及函数使用:fork,exec,wait,waitpid,exit,getpid,getppid,sleep,pause,setenv,unsetenv
  • 理解数组指针、指针数组、函数指针、指针函数的区别
  • 理解信号机制:kill,alarm,signal,sigaction
  • 掌握管道和I/O重定向:pipe, dup, dup2

重点分析老师所给我们的代码:

图2:

教材学习内容总结

① 了解异常及其种类

异常:处理器中的变化(事件)触发从应用程序到异常处理程序的突发的控制转移。异常的一部分由硬件实现,一部分由操作系统实现,它就是控制流中的突变,用来响应处理器状态的某些变化。

种类:被零除,缺页,存储器访问违例,断点,算术溢出;系统调用,来着外部I/O设备的信号

处理:异常处理

异常表:当处理器检测到有事件发生时,它会通过跳转表,进行一个间接过程调用(异常),到异常处理程序。

异常号:系统中可能的某种类型的异常都分配了一个唯一的非负整数的异常号。异常号是到异常表中的索引。

异常处理程序完成处理后,根据异常事件的类型会(执行一种):

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

异常可以分为四类:

图1:

中断:是异步发生的,硬件中断不由任何指令造成,所以说是异步的。硬件中断的异常处理程序称为中断处理程序。

陷阱、故障和终止是同步发生的,称为故障指令

陷阱:是有意的异常,主要用来在用户程序和内核之间提供一个像过程一样的接口,称为系统调用。处理器提供了 syscall n 指令来满足用户向内核请求服务 n , syscall 指令会导致一个到异常处理程序的陷阱,处理程序调用适当的内核程序。普通函数运行在用户模式,而系统调用运行在内核模式。

故障:由错误引起,如缺页异常。故障发生时,处理器将控制转移给故障处理程序,如果处理程序能够修正错误,就将控制返回到故障指令,重新执行;否则处理程序返回到内核的 abort 例程, abort 终止应用程序。

终止:是不可恢复的致命错误的结果,主要是一些硬件错误。终止处理程序将控制返回到 abort 例程,abort 终止应用程序。

Linux/IA32系统中的异常一共有256种

图2:

IA32系统中的异常示例

  • 除法错误:除零或除法结果太大,Unix终止程序,报告为浮点异常。
  • 一般保护故障:通常为引用未定义的虚拟存储器区域或写一个只读的文本段,Unix终止程序,报告为段故障。
  • 缺页:将物理存储器相应的页面映射到虚拟存储器的页面,重新执行故障指令。
  • 机器检查:检测到致命的硬件错误。

② 理解进程和并发的概念

进程

进程是一个执行中程序的实例。系统中每个程序都是运行在某个进程的上下文中的。上下文由程序正确运行所需的状态组成,包括程序的存放在存储器中的代码和数据、栈、通用目的寄存器的内容、程序计数器、环境变量和打开文件描述符的集合。

shell中运行程序时,shell会创建一个新的进程,然后在新进程的上下文中运行可执行目标文件。应用程序还能创建新进程。

进程给应用程序提供了两个关键抽象:

  1. 独立的逻辑控制流,提供程序独占处理器的假象。
  2. 私有的地址空间,提供程序独占存储器系统的假象。

并发流

含义:一个逻辑流的执行在时间上与另一个流重叠。这两个流并发的运行。

两个流并发的运行在不同的处理机核或者计算机上。并行流并行的运行,并行的执行。

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

用户模式和内核模式

区别:有无模式位,有的话就是内核模式,可以执行指令集中的所有指令,访问系统中任何存储器位置;没有就是用户模式。

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

  • 上下文:内核重新启动一个被抢占的进程所需的状态。由一些对象的值组成:通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈
  • 内核数据结构:页表、进程表、文件表
  • 调度和调度器
  • 上下文切换机制
  • 可能发生上下文切换的原因

③ 进程创建和控制的系统调用及函数使用

创建和终止进程

进程三种状态:运行、停止、终止

创建进程

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

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

终止进程

用exit函数。

进程终止时,并不会被立即清除,而是等待父进程回收,称为僵死进程。父进程回收终止的子进程时,内核将子进程退出状态传给父进程,然后抛弃该进程。如果回收前父进程已经终止,那么僵死进程由 init 进程回收。

回收子进程

回收子进程可以用 waitwaitpid 等函数。

waitpid函数:成功返回子进程PID,如果WNOHANG,返回0,其他错误返回-1.

特例:如果父进程没有回收它的子进程就终止了,那么内核就会安排init函数来回收它们,init函数的返回值是1

wait函数:是waitpid函数的简单版本,wait(&status)等价于waitpid(-1,&status,0)。成功返回子进程pid,出错返回-1。

④ 关于指针数组与数组指针的区别

数组指针(也称行指针)

定义 int (*p)[n]😭)优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。

如要将二维数组赋给一指针,应这样赋值:

int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
p=a;//将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++;   //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]

数组指针也称指向一维数组的指针,亦称行指针。

指针数组

定义 int p[n];[]优先级高,先与p结合成为一个数组,再由int说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 p=a; 这里p表示指针数组第一个元素的值,a的首地址的值。

如要将二维数组赋给一指针数组:

int *p[3];
int a[3][4];
p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针
for(i=0;i<3;i++)
p[i]=a[i];

这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2] 所以要分别赋值。

⑤ 函数指针和指针函数的区别

指针函数是指带指针的函数,即本质是一个函数。函数返回类型是某一类型的指针

类型标识符 函数名(参数表) ;具体格式:int f(x,y);

首先它是一个函数,只不过这个函数的返回值是一个地址值。指针函数一定有函数返回值,而且在主调函数中,函数返回值必须赋给同类型的指针变量。例如:

01.float *fun();  
02.float *p;  
03.p = fun(a);  

函数指针是指向函数的指针变量,即本质是一个指针变量。

指向函数的指针包含了函数的地址,可以通过它来调用函数。声明格式如下: 类型说明符 (*函数名)(参数)

使用的时候:

01.int (*f)(int x); /*声明一个函数指针 */  
02.f=func; /*将func函数的首地址赋给指针f */  

⑥ 理解信号机制

信号机制:

信号是一种更高层软件形式的异常,它允许进程中断其他进程。一个信号即一条信息,通知进程一个某种类型的事件已经在系统中发生了。

下表是Linux系统中的信号:

图3:

发送信号

给进程基于进程组的概念。进程组由一个正整数ID标识,每个进程只属于一个进程组。

用 kill 命令向其他进程发送任意信号,给定的PID为负值时,表示发送信号给进程组ID为PID绝对值的所有进程。

进程可以用 kill 函数发送信号给任意进程(包括自己)。

接收信号

每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号集。每个可能的信号都有一位屏蔽字,对应位设置时表明信号当前是被阻塞的。用 sigprocmask 函数检测和更改当前信号屏蔽字。

每种信号都有默认行为,可以用 signal 函数修改和信号关联的默认行为(除 SIGSTOP 和 SIGKILL 外):

#include <signal.h>

typedef void (*sighandler_t)(int);

/** 改变和信号signum关联的行为
 * @return  返回前次处理程序的指针,出错返回SIG_ERR */
sighandler_t signal(int signum, sighandler_t handler);
参数说明:

signum信号编号。handler指向用户定义函数,也就是信号处理程序的指针。或者为:

signal 的语义和实现有关,最好使用 sigaction 函数代替它。

例:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>

void w_error(const char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}

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

int main()
{
if (signal(SIGINT, handler) == SIG_ERR)
w_error("signal error");
pause();
printf("come back\n");
exit(0);
}

老师所给代码编译结果

argv文件夹

首先对argv文件夹里的文件进行分析:argv文件夹中的主要程序是argtest.c

那么,我们需要先用-c将所有.c的文件编译成后缀为.o的文件,然后一起编译成可执行文件

图4:

图5:

代码分析:

#include <stdio.h>
#include <stdlib.h>
#include "argv.h"//该函数库中包括freemakeargv.c及makeargv.c函数的调用

int main(int argc, char *argv[]) 
{
   char delim[] = " \t";
   int i;
   char **myargv;//定义myargv,二级指针
   int numtokens;

   if (argc != 2)//argc是可执行文件,如果输入其个数不等于2,就输出标准错误 
   {
fprintf(stderr, "Usage: %s string\n", argv[0]);
return 1;
   }   
  if ((numtokens = makeargv(argv[1], delim, &myargv)) == -1) 
  {
fprintf(stderr, "Failed to construct an argument array for %s\n", argv[1]);
//当无法构造一个参数数组时,输出这条语句。
return 1;
   } 
   printf("The argument array contains:\n");
   for (i = 0; i < numtokens; i++)
printf("%d:%s\n", i, myargv[i]);
   execvp(myargv[0], myargv);

   return 0;
}

env文件夹

显而易见,其主要函数就是environ.c,其主要作用是简单打印环境变量表。

图6:

接下来执行程序environvar.c

图7:

代码分析:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
printf("PATH=%s\n", getenv("PATH"));
//getenv函数用来取得参数PATH环境变量的值,执行成功则返回该内容的指针
setenv("PATH", "hello", 1);
printf("PATH=%s\n", getenv("PATH"));
#if 0
printf("PATH=%s\n", getenv("PATH"));
setenv("PATH", "hellohello", 0);
printf("PATH=%s\n", getenv("PATH"));


printf("MY_VER=%s\n", getenv("MY_VER"));//get版本信息
setenv("MY_VER", "1.1", 0);
printf("MY_VER=%s\n", getenv("MY_VER"));
#endif
return 0;
}

fifo文件夹

  1. FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。
  2. FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
  3. FIFO往往都是多个写进程,一个读进程。

首先我们运行consumer.c:

图8:

一开始没看代码的时候,其输出我们也一头雾水,现在对代码进行分析:

代码分析:

图9:

进行了代码分析之后,我们确定了,其输出是进程pid号和管道读端的pipe_fd

接下来运行管道读段producer.c

图10:

继续进行代码分析:

代码分析:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO_NAME "/tmp/myfifo"
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 * 10)

int main()
{
int pipe_fd;
int res;
int open_mode = O_WRONLY;

int bytes = 0;
char buffer[BUFFER_SIZE + 1];

if (access(FIFO_NAME, F_OK) == -1) {//检查文件是否有相应的权限
res = mkfifo(FIFO_NAME, 0777);//依据FIFO_NAME创建fifo文件,0777依次是相应权限
if (res != 0) {
fprintf(stderr, "Could not create fifo %s \n",
FIFO_NAME);
exit(EXIT_FAILURE);
}
}

printf("Process %d opening FIFO O_WRONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d result %d\n", getpid(), pipe_fd);

if (pipe_fd != -1) {
while (bytes < TEN_MEG) {
res = write(pipe_fd, buffer, BUFFER_SIZE);
if (res == -1) {
fprintf(stderr, "Write error on pipe\n");
exit(EXIT_FAILURE);
}
bytes += res;
}
close(pipe_fd);
} else {
exit(EXIT_FAILURE);
}

printf("Process %d finish\n", getpid());
exit(EXIT_SUCCESS);
}

pipe文件夹

调用pipe来创建管道并将其两端连接到两个文件描述符,array[0]为读数据端的文件描述符,而array[1]则为写数据端的文件描述符,内部则隐藏在内核中,进程只能看到两个文件描述符。

listargs.c 证明了shell并不将重定向标记和文件名传递给程序

图11:

接下来运行主要的代码:pipe.c

图12:

对pipe.c进行代码分析:

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

#define oops(m,x)   //定义oops,当程序运行出现错误时
{ perror(m); exit(x); }

int main(int ac, char **av)
{
int thepipe[2], newfd,pid;  

if ( ac != 3 ){//输入的命令长度不等于3
fprintf(stderr, "usage: pipe cmd1 cmd2\n");
exit(1);
}
if ( pipe( thepipe ) == -1 )//以下是各种错误   
oops("Cannot get a pipe", 1);

if ( (pid = fork()) == -1 ) 
oops("Cannot fork", 2);

if ( pid > 0 ){ 
close(thepipe[1]);  

if ( dup2(thepipe[0], 0) == -1 )
oops("could not redirect stdin",3);

close(thepipe[0]);  
execlp( av[2], av[2], NULL);
oops(av[2], 4);
}

close(thepipe[0]);  

if ( dup2(thepipe[1], 1) == -1 )
oops("could not redirect stdout", 4);

close(thepipe[1]);  
execlp( av[1], av[1], NULL);
oops(av[1], 5);
}

pipedemo.c 管道

展示了如何创建管道并使用管道来向自己发送数据

编译及运行结果:

图13:

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

int main()
{
int len, i, apipe[2];   //apipe数组中存储两个文件的描述符
charbuf[BUFSIZ];

if ( pipe ( apipe ) == -1 ){//pipe语句出错,此时返回无法创建管道
perror("could not make pipe");
exit(1);
}
printf("Got a pipe! It is file descriptors: { %d %d }\n", 
apipe[0], apipe[1]);


while ( fgets(buf, BUFSIZ, stdin) ){  //从标准输入读入数据,放到缓冲区
len = strlen( buf );
if (  write( apipe[1], buf, len) != len ){   
//向apipe[1](即管道写端)写入数据

perror("writing to pipe");  
break;  
}
for ( i = 0 ; i<len ; i++ )  //清理缓冲区
buf[i] = 'X' ;
len = read( apipe[0], buf, BUFSIZ ) ;   //从apipe[0](即管道读端)读数据   
if ( len == -1 ){   
perror("reading from pipe");
break;
}
if ( write( 1 , buf, len ) != len ){ //把从管道读出的数据再写到标准输出
perror("writing to stdout");
break;  
}
}
}

pipedemo2.c 使用管道向自己发送数据

说明了如何将pipefork结合起来,创建一对通过管道来通信的进程

程序的运行十分有趣,持续间断地发送test请求

图14:

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


#define CHILD_MESS  "I want a cookie\n"
#define PAR_MESS"testing..\n"
#define oops(m,x)   { perror(m); exit(x); }   //还可以这样宏定义语句块

main()
{
int pipefd[2];  
int len;
charbuf[BUFSIZ];
int read_len;

if ( pipe( pipefd ) == -1 )  // 创建一个管道:apipe[0]读,apipe[1]写  
oops("cannot get a pipe", 1);

switch( fork() ){
case -1:
oops("cannot fork", 2);

case 0: 
len = strlen(CHILD_MESS);
while ( 1 ){
if (write( pipefd[1], CHILD_MESS, len) != len )
oops("write", 3);
sleep(5);
}

default:
len = strlen( PAR_MESS );
while ( 1 ){
if ( write( pipefd[1], PAR_MESS, len)!=len )
oops("write", 4);
sleep(1);
read_len = read( pipefd[0], buf, BUFSIZ );
if ( read_len <= 0 )
break;
write( 1 , buf, read_len );
}
}
}

运行stdinredir1.c

图15:

是一个输入输出的循环

我们继续运行stdinredir2.c

然后进一步分析这两个程序的特性

图16:

代码分析:

通过对比两个侧滑盖内需得到:

stdinredir1.c 将stdin定向到文件

而stdinredir2open..dup2..close

只是dup2(fd,0)将close(0),dup(fd)合在一起

testtty

运行结果:

图17:

signal文件夹

sigactdemo.c

运行结果:
图18:

代码分析sigactdemo.c

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#define INPUTLEN100
void inthandler();  
int main()
{
struct sigaction newhandler;//用sigaction结构来定义newhandler 
sigset_t blocked;   //信号集,用来描述信号的集合,与信号阻塞相关函数配合使用
char x[INPUTLEN];
newhandler.sa_handler = inthandler; //函数指针
newhandler.sa_flags = SA_RESTART|SA_NODEFER
|SA_RESETHAND;  //sa_flags是一个位掩码。这里,第一个参数使得被信号打断的一些原语“正常返回”
sigemptyset(&blocked);  
sigaddset(&blocked, SIGQUIT);   
newhandler.sa_mask = blocked;   
if (sigaction(SIGINT, &newhandler, NULL) == -1)
perror("sigaction");
else
while (1) {
fgets(x, INPUTLEN, stdin);
printf("input: %s", x);
}
return 0;
}
void inthandler(int s)
{
printf("Called with signal %d\n", s);
sleep(s * 4);
printf("done handling signal %d\n", s);
}

其中,sigaction的结构定义如下

struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}

运行sigaction2.c

结果如下:

图19:

本周感想

本周学习任务比较重,是由于既有大量代码的分析(是上一周量的两倍多),又要加上第八章和第十章知识的学习。运行完所有文件夹中的代码,发现真的没法再分析下去了。其他课业任务也同样重要。所以我选择先把process根目录下的代码先放一放,宁少勿糙。

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第七周 1300/1750 11/11 140/140
第八周 1700/2000 13/13 160/160
第九周 2000/2400 14/15 180/180
第十周 2500/2800 15/17 0/200

| 第十周 | 2500/3000 | 15/17 | 0/200 | |

参考资料

posted on 2016-11-27 21:47  20145314郑凯杰  阅读(196)  评论(0编辑  收藏  举报

导航