信息安全系统设计基础第十三周学习总结
【学习时间:5小时】
【学习内容:CHAPTER12——并发编程】
一、课本知识点梳理
1.并发
- 概念:只要逻辑控制流在时间上重叠,那么就可以称为并发。
- 意义:
- 访问慢速设备(如I/O设备)
- 与人交互:每次用户进行某种操作的请求的时候,一个独立的逻辑并发流被用来处理这个程序。
- 通过推迟工作来降低延迟
- 服务多个网络客户端
- 进程:每个逻辑控制流都是一个进程,由内核进行调度和维护。进程间的通信也必须有显式的通信机制。
- I/O多路复用:在并发编程中,一个程序在上下文中调用它们自己的逻辑流。因为程序是一个单独的进程,所以所有的流共享一个地址空间。
- 线程:像进程一样由内核调用,像I/O多路复用一样共享同样的虚拟地址空间
2.构造并发服务器过程
- (假设是一个服务器端两个客户端,服务器端始终监听)服务器正在监听一个描述符3,客户端1向服务器端提出申请,服务器端返回一个已经建立连接描述符4;
- 服务器派生一个子进程处理该连接。子进程拥有从父进程服务器描述符列表中完整的拷贝。此时,父进程与子进程需要各自切断连接:父进程切断它的描述符列表中的连接描述符4,子进程切断它拷贝的监听描述符3;
- 服务器接收新的客户端2的连接请求,同样返回连接描述符5,派生一个子进程;
- 服务器端继续等待请求,两个子进程并发地处理客户端连接。
- 代码
【直到父子进程都关闭,到客户端的连接才会终止】
3.基于I/O多路复用的并发编程
- 说明:如果服务器要既能相应客户端的连接请求,又能响应用户的键盘输入,那么会产生等待谁的冲突。使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生之后,才将控制返回给进程。
-
select函数
- 原型:int select(int n,fd_set *fdset,UNLL,NULL,NULL);返回已经准备好的描述符的非零的个数,若出错则为-1
- 举例:
- 说明:select函数有两个输入,一个称为读集合的描述符集合和该集合的基数。select函数会一直阻塞直到该集合中有一个或者多个描述符准备好可以读了。当且仅当一个从该描述符读取字节的请求不会被拒绝的时候,该描述符才是准备好可以读的。另外,每次调用select函数的时候都要更新读集合。
4.基于I/O多路复用的并发echo服务器
- 解释:
- 综述:服务器借助select函数检测输入事件的发生。当每个已经连接的描述符准备好可读的时候,服务器就为相应的状态机执行转移(即,从描述符读和写回一个文本行)。【什么是写回?可以参考下方的代码中的check_client函数的解释】
- 分步解释:
- 活动客户端的集合【注:这里的意思应该就是可能并且可以发送连接请求进行文本行读取的客户端在进行输入的时候需要用到的数据】维护在一个pool结构里。
- 调用init函数初始化池,然后无限循环。
- 在循环的每次迭代中,服务器端调用select函数来检测两种不同类型的输入事件:来自一个新客户的连接请求到达【先打开连接,然后调用add_client函数将其添加到池里】;一个已经存在的客户的已连接描述符准备好可以读了。【因为是无限循环,所以每次循环开始的时候服务器都会把现在active的客户端描述符赋值给表示已准备好的描述符中去,然后检测有没有“新来的”客户端(如果有就放入池中),最后是统一把已经准备好的描述符的文本行写回】。
5.状态机
I/O多路复用可以作为并发事件驱动程序的基础;在并发事件中,流是因为某种事件而前进的。一般是将逻辑流模型化为状态机。
一个状态机可以简化为一组 状态、输入事件和转移(将前两者映射到状态) 。(自循环是同一输入和输出状态之间的转移)
通常把状态机画成有向图,其中节点表示状态,有向弧表示转移,弧上的标号表示输入事件。对于每一个新的客户端k,基于I/O多路复用的并发服务器会创建一个新的状态机sk,并将它和已连接描述符dk连接起来。
6.基于线程的并发编程
- 线程,就是运行在进程上下文中的逻辑流,由内核自动调度。每个线程都有它自己的线程上下文,包括唯一的一个整数线程ID、栈、栈指针等。所有运行在一个进程里的线程共享该进程的地址空间。
- 线程执行模型
- 每个进程开始生命周期的时候都是单一线程,这个线程称为主线程;在某一时刻,主线程创建一个对等线程,从这个时间点开始,两个线程并发地运行。
- 与进程的区别:
- 线程的上下文比进程上下文小得多,而且上下文切换比进程快得多;
- 线程不像进程那样有着严格的父子层次,主线程和其他线程的区别在于前者是进程中第一个运行的线程(仅此而已)————很重要的影响是,线程可以杀死它的任何对等线程。
7.Posix线程
- Posix线程(Pthreads)是在C程序中处理线程的一个标准接口,在大多数unix系统中都可以调用。它定义了约60个函数,允许程序创建、杀死和回收线程,与对等线程安全地共享数据等。
-
- 介绍:主线程创建一个对等线程,然后等待它的终止。对等线程输出“Hello,world! ”并终止。当主线程检测到对等线程终止以后,它就通过调用exit终止该进程。
- 分析:线程的代码和本地数据被封装在一个线程示例(thread routine)中。代码第2行,每个线程都用一个通用指针为输入,并返回一个通用指针;代码第7行,主线程创建对等线程,在pthread_create函数返回之后,主线程和对等线程才同时运行。
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操作。
【注意:P中的测试和减1操作是不可分割的;V中的重启和加1操作也是不可分割的。此外,V的定义中没有说明等待线程被重启的顺序,也就是说,当多个线程在等待同一个信号量的时候,你不能预测V操作要重启哪个线程】
11.用信号量实现互斥的代码
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);
}
12.利用互斥信号量来调度共享资源——读者&写者问题
- 概述:一组并发的线程要访问一个共享对象。有些线程只读对象,其他线程只修改对象。前者叫做读者,后者叫做写者。写者必须拥有对对象的独占的访问,而读者可以和无限多的其他的读者共享对象。
- 解决方案:信号量w控制对访问共享对象的临界区的访问。信号量mutex保护对共享变量readcnt的访问,readcnt统计当前在临界区中的读者的数量。每当一个写者进入临界区的时候,它对w加锁,离开的时候对w解锁。这就保证了任意一个时刻缓冲区中最多有一个写者。另一方面,只有第一个进入临界区的读者会对w加锁,最后一个离开临界区的读者对w解锁。这就意味着,读者可以没有障碍地进入临界区。
- 代码:
二、课后练习题
1.练习题12.1
第33行代码,父进程关闭了连接描述符后,子进程仍然可以使用该描述符和客户端通信。为什么?
当父进程派生子进程时,它得到一个已经连接描述符的副本,并将相关文件表的引用计数从1加到2;父进程关闭它的描述符副本时,引用计数从2减少1(内核不会关闭文件知道它的引用计数变为0)。这样连接仍然保持打开
2.练习题12.3
如果在上述程序阻塞在对select的调用的时候,键入ctrl-d,会发生什么?
(参考答案)如果从一个描述符中读取一个字节的要求不会被阻塞,那么它就是准备好可以读的了。对于EOF来说也是如此。假如 EOF在一个描述符上为真,那么该描述符也准备好可以读了,因为读操作会立即返回一个零返回码,表示EOF。因此,键入ctrl-d会导致select函数返回。
三、心得
这已经是本书的最后一章了,回顾之前的学习,发现自己的很多改变都是在不知不觉中发生的。比如,在本章的学习中,我已经能够很自觉地百度各种不会的“专有名词”或者是翻找书本之前的内容。