信息安全设计基础第十三周学习总结

信息安全系统设计基础第十三周学习总结

【学习时间:7小时】

【学习内容:CHAPTER12——并发编程】

一、课本内容理解(个人理解部分用【】标出)

1.并发的意义

  • 概念:只要逻辑控制流在时间上重叠,那么就可以称为并发。
  • 意义
    • 访问慢速设备(如I/O设备):【CPU可以在这样的慢速中“腾出手”再去做其他事情,使自己保持“繁忙”。】
    • 与人交互:每次用户进行某种操作的请求的时候【用户不会在意在自己进行操作的时候系统是否还处理着其他程序】,一个独立的逻辑并发流被用来处理这个程序。
    • 通过推迟工作来降低延迟
    • 服务多个网络客户端
    • 进程:每个逻辑控制流都是一个进程,由内核进行调度和维护。进程间的通信也必须有显式的通信机制。【每个进程都有独立的虚拟地址空间,发生交流的时候肯定要有机制】
    • I/O多路复用:在并发编程中,一个程序在上下文中调用它们自己的逻辑流。因为程序是一个单独的进程,所以所有的流共享一个地址空间。【即,对于从该进程所属的数据空间内进出的I/O道路上“运输”的数据是要提供给多个逻辑流的】
    • 线程:像进程一样由内核调用,像I/O多路复用一样共享同样的虚拟地址空间

2.构造并发服务器过程

  1. (假设是一个服务器端两个客户端,服务器端始终监听)服务器正在监听一个描述符3,客户端1向服务器端提出申请,服务器端返回一个已经建立连接描述符4;
  2. 服务器派生一个子进程处理该连接。子进程拥有从父进程服务器描述符列表中完整的拷贝。此时,父进程与子进程需要各自切断连接:父进程切断它的描述符列表中的连接描述符4,子进程切断它拷贝的监听描述符3;
  3. 服务器接收新的客户端2的连接请求,同样返回连接描述符5,派生一个子进程;
  4. 服务器端继续等待请求,两个子进程并发地处理客户端连接。

【关于步骤2的解释:父进程返回的连接描述符在连接建立之后就应该交给子进程了,父进程若不关闭与子进程相关的描述符拷贝,可能造成(子进程)的存储泄露;子进程从父进程拷贝的监听描述符对于子进程本身而言也毫无用处。这就好像是母亲和胎儿的关系,只有剪短脐带,胎儿才能独立成长】

3.上述过程的代码描述

(图1)

  • 父子进程必须各自关闭它们的connfd,【也就是循环体内外各有一个Close(connfd)】
  • 直到父子进程都关闭,到客户端的连接才会终止

4.基于I/O多路复用的并发编程

  • 背景:如果服务器要既能相应客户端的连接请求,又能响应用户的键盘输入,那么会产生等待谁的冲突
  • 思想:使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生之后,才将控制返回给进程。
  • select函数
    • 原型:int select(int n,fd_set *fdset,UNLL,NULL,NULL);返回已经准备好的描述符的非零的个数,若出错则为-1
    • 解释:【在反复阅读之后,我逐步理清了select函数的思想。针对上文中说的“先等待谁”的矛盾,select函数做到了“不偏不倚”。当它的参数集合 *fdset 中的一个或者多个描述符显示为准备好可以读的时候,select函数就返回该准备好的描述符的集合(严格来说不是它返回,而是修改了传进来的集合指针使得其指向这些准备好的描述符的集合)。这样一来,select函数就用“统一”的标准对待可能发生冲突的所有事件,避免了“亲疏有别”。】
    • 举例: 
    • (图3)
      • 说明:select函数有两个输入,一个称为读集合的描述符集合和该集合的基数。select函数会一直阻塞直到该集合中有一个或者多个描述符准备好可以读了。当且仅当一个从该描述符读取字节的请求不会被拒绝的时候,该描述符才是准备好可以读的。另外,每次调用select函数的时候都要更新读集合。
      • 解释:首先,代码第17行和第19行分别打开了一个监听描述符和一个空的读集合(既能够响应客户端的连接请求,又可以接收用户的键盘输入);然后,代码第20行和21行定义了由描述符0(标准输入)和3(监听描述符)组成的读集合;之后,进入典型的循环状态,由select函数进行“控制”。

5.状态机

  • 引入:I/O多路复用可以作为并发事件驱动程序的基础;在并发事件中,流是因为某种事件而前进的。一般是将逻辑流模型化为状态机。
  • 概念:一个状态机可以简化为一组 状态、输入事件和转移(将前两者映射到状态) 。(自循环是同一输入和输出状态之间的转移)
  • 表示方法:通常把状态机画成有向图,其中节点表示状态,有向弧表示转移,弧上的标号表示输入事件。对于每一个新的客户端k,基于I/O多路复用的并发服务器会创建一个新的状态机sk,并将它和已连接描述符dk连接起来。

6.基于I/O多路复用的并发echo服务器

  • 代码: 

(图4,5)

  • 解释:
    • 综述:服务器借助select函数检测输入事件的发生。当每个已经连接的描述符准备好可读的时候,服务器就为相应的状态机执行转移(即,从描述符读和写回一个文本行)。【什么是写回?可以参考下方的代码中的check_client函数的解释】
    • 分步解释:
      • 活动客户端的集合【注:这里的意思应该就是可能并且可以发送连接请求进行文本行读取的客户端在进行输入的时候需要用到的数据】维护在一个pool结构里。
      • 调用init函数初始化池,然后无限循环。【我认为这一部分可以截止到29行;第26行是把输入的第二个参数代表的端口号转换成int型取出来】
      • 在循环的每次迭代中,服务器端调用select函数来检测两种不同类型的输入事件:来自一个新客户的连接请求到达【第36——38行,先打开连接,然后调用add_client函数将其添加到池里】;一个已经存在的客户的已连接描述符准备好可以读了。【我理解这段代码是这样的:因为是无限循环,所以每次循环开始的时候服务器都会把现在active的客户端描述符赋值给表示已准备好的描述符中去,然后检测有没有“新来的”客户端(如果有就放入池中),最后是统一把已经准备好的描述符的文本行写回】。
    • 调用函数
      • init_pool:初始化的时候,已连接描述符集合是空的,而且监听描述符是读集合中唯一的描述符
      • check_client:如果服务器成功地从描述符读取了一个文本行,那么我们就将该文本行送回到客户端。如果客户端关闭这个连接中的它那边的一端 ,服务器会检测到EOF,然后也关闭自己这边的连接端,并从池中删除这个描述符。

7.用信号量同步线程

  • 代码:

(图6,7)

  • 结果:代码运行之后,会出现我们意想不到的错误。
  • 探究:研究哪里出错的话,关注下列代码的汇编形式

    for(i = 0;i<niters;i++)
        cnt++;
    

    当badcnt.c中的两个对等现场在一个单处理器上并发运行时,机器指令以某种顺序一个一个地完成。然而,这其中的一些会将长生正确的结果,其他的则不会。一般而言,你没有办法预测操作系统是否将为你的线程选择一个正确的顺序

8.进度图

  • 概念:将n个并发线程的执行模型化为一条n维笛卡儿空间中的轨迹线。每条轴k对应于线程k的进度。每个点(I1,I2,I3……,In)代表线程k已经完成了指令IK这一状态。进度图将指令执行模型化为从一种状态到另一种状态的转换。转换被表示为从一点到相邻点的有向边。

9.互斥

我们要确保每个线程在执行它的临界区中的指令的时候,拥有对共享变量的互斥的访问。通常这种现象称为互斥。在进度图中,两个临界区的交集形成的状态空间区域称为不安全区(不包括毗邻不安全区的那些点)。

10.信号量

  • 概念:信号量s是具有非负整数值的全局变量,只能由两种特殊的操作来处理
  • 操作
    • P(s):如果s为0,那么P将s减1,并且立即返回。如果s为零,那么就挂起这个线程,直到s为非零;而V操作会重启这个线程。在重启之后,P操作将s减一,并将控制返回给调用者
    • V(s):V操作将s加一;如果有任何线程阻塞在P操作等待s变成非0,那么V操作会重启这些线程中的一个,然后该线程将s减1,完成它的P操作。

【这里,信号量s相当于标识某个互斥资源是否可用的标志。当s为1的时候,互斥资源是可用的;否则,不可用】

【注意:P中的测试和减1操作是不可分割的;V中的重启和加1操作也是不可分割的。此外,V的定义中没有说明等待线程被重启的顺序,也就是说,当多个线程在等待同一个信号量的时候,你不能预测V操作要重启哪个线程】

11.使用信号量来实现互斥

  • 引入:以提供互斥为目的的二元信号量又叫做互斥锁。在一个互斥锁上执行P操作的过程叫做互斥加锁;执行V操作的过程叫做互斥解锁。一个被用作一组可用资源的计数器的信号量叫做计数信号量。
  • (图8)

12.用信号量实现互斥的代码

volitale int cnt = 0;
sem_t mutex;
Sem_init(&mutex,0,1);//mutex = 1;
for(int i =0;i<niters;i++)
{
    P(&mutex);
    cnt++;
    V(&mutex);
}

【理解:我认为这段代码相当于在cnt++这句代码前后加了两道“保险杠”;前面的保险杠负责提前锁上信号量,防止别的进程“拿去用”,后面的保险杠与前面的,负责解锁信号量以便下一次循环正常使用(同时V操作也是依次循环中的最后一步)】

13.利用互斥信号量来调度共享资源——生产者&消费者问题

【我觉得这里的生产者与消费者问题可以参考我们的实验二中的多线程编译部分的代码;那里面已经把这个问题涉及得比较全面了。然而,这里的解释更加明确准确一些。】

  • 概述:生产者和消费者共享一个n个槽的有限缓冲区。生产者反复地生产新的项目,并把它们插入到缓冲区中;消费者不断地从缓冲区中读取这些项目,然后消费它们。
  • 要点:
    • 插入和取出项目都涉及更新共享变量,所以必须保证对缓冲区的访问是互斥的;
    • 同时,还要调度对缓冲区的访问,如果缓冲区是满的,那么生产者必须等待直到某个槽位可用;同样的,如果缓冲区是空的,消费者必须等待直到有一个项目变得可用。
  • 代码实现

    • 结构体:

      typedef struct{
          int *buf;//指向槽位的指针
          int n;//n个槽位
          int front;//指向数组的第一项
          int rear;//指向数组的最后一项
          sem_t mutex;//提供互斥的缓冲区访问的信号量
          sem_t slots;//记录空槽位数量
          sem_t items;//记录可用项目的数量
      }sbuf_t;
      
    • 代码:

  • (图9,10)

    • 解释:
      • 【这段代码结构很清晰。首先,第一个函数像往常一样都是初始化函数。在内存的随机存储区中申请一个n个int型变量的空间(malloc函数与calloc函数最大的区别就是后者可以将这些连续的区间初始化为零)。然后front = rear,类似于队列初始化的时候,因为没有元素(槽位)被使用,所以就让队首=队尾,以此表示队列未被使用】
      • 【subf_deinit函数从名字上就可以看出来是“去初始化”,也就是在生产者/消费者使用完这个位置之后,释放缓冲区存储的】
      • 【subf_insert函数完成以下步骤:等待一个可用的槽位(第24行,&slots是可用空槽位的地址),对互斥锁加锁(第25行,锁上了mutex,也就是相当于把“此房间不可访问”的牌子挂出去),添加项目(第26行,把当前队尾的位置填充上item),对互斥锁解锁(第27行,是V操作),然后宣布一个新项目可用(第28行,重启进程)】
      • 【subfremove函数,与上面的subfinsert类似,只不过最中间的核心步骤是从缓冲区中取出项目】

14.利用互斥信号量来调度共享资源——读者&写者问题

  • 概述:一组并发的线程要访问一个共享对象。有些线程只读对象,其他线程只修改对象。前者叫做读者,后者叫做写者。写者必须拥有对对象的独占的访问,而读者可以和无限多的其他的读者共享对象。
  • 解决方案:信号量w控制对访问共享对象的临界区的访问。信号量mutex保护对共享变量readcnt的访问,readcnt统计当前在临界区中的读者的数量。每当一个写者进入临界区的时候,它对w加锁,离开的时候对w解锁。这就保证了任意一个时刻缓冲区中最多有一个写者。另一方面,只有第一个进入临界区的读者会对w加锁,最后一个离开临界区的读者对w解锁。这就意味着,读者可以没有障碍地进入临界区。
  • 代码:

(图11)

二、练习题筛选

1.练习题12.1

第33行代码,父进程关闭了连接描述符后,子进程仍然可以使用该描述符和客户端通信。为什么?

当父进程派生子进程时,它得到一个已经连接描述符的副本,并将相关文件表的引用计数从1加到2;父进程关闭它的描述符副本时,引用计数从2减少1(内核不会关闭文件知道它的引用计数变为0)。这样连接仍然保持打开

2.练习题12.3

如果在上述程序阻塞在对select的调用的时候,键入ctrl-d,会发生什么?

(参考答案)如果从一个描述符中读取一个字节的要求不会被阻塞,那么它就是准备好可以读的了。对于EOF来说也是如此。假如 EOF在一个描述符上为真,那么该描述符也准备好可以读了,因为读操作会立即返回一个零返回码,表示EOF。因此,键入ctrl-d会导致select函数返回。

3.练习题12.9

设p表示生产者数量,c表示消费者数量,n表示以项目单元为单位的缓冲区大小。对于下面的美国场景,指出subfinsert和subfremove中的互斥信号量是否是必需的。

A.p =1,c =1,n>1

B.p =1,c=1,n=1

C.p>1,c>1,n=1

A:是。因为生产者和消费者会并发地访问缓冲区

B:不是。因为n=1,一个非空的缓冲区就相当于一个满的缓冲区。当缓冲区包含一个项目的时候,生产者就已经被阻塞了;当缓冲区为空的时候,消费者就被阻塞了。所以在任意时刻,只有一个线程可以访问缓冲区,不必加互斥锁。

C:不是。同上。

三、疑问

1.P650代码如下

(图2) sigchld_handler函数如何实现回收多个僵死子进程的?(补充:通过查阅,WNOHANG参数的意义是,如果等待的子进程没有结束,就停止子进程并返回-1.否则,返回子进程的PID。然而,while循环的意思是只针对pid大于0即已经停止的进程?)

【已经自己解决:waitpid的作用就是将那些停止的子进程去除。所以,这里只针对那些pid大于0 的进程执行一次waitpid函数就可以】

四、心得

感觉本学期的课程学习过程过得很快,不知不觉之间十二章的学习就基本结束了。作为最后的总结“升华”章,第十二章的很多内容是和前面联系在一起的;现在看来可以理解的知识,如果是在刚开学的时候去读,就未必理解得了。也从一个角度说明“厚积薄发”吧。

posted @ 2015-12-06 20:21  5216  Views(261)  Comments(2Edit  收藏  举报