2017-2018-1 20155323 《信息安全系统设计基础》第十三周学习总结
找出全书你认为最重要的一章,深入重新学习一下,要求:
完成这一章所有习题
详细总结本章要点
给你的结对学习搭档讲解你的总结并获取反馈
参考上面的学习总结模板,把学习过程通过博客(随笔)发表,博客标题“学号 《信息安全系统设计基础》第十三周学习总结”,博客(随笔)要通过作业提交。
并发编程
我认为十二章并发编程时全书最重要的一章,因为整本书从程序的结构和执行,讲到如何在系统上运行程序,再到如何实现程序间的交互和通信,并发编程毫无疑问是最后的重中之重。
12.1 基于进程的并发编程
- 父子进程的已连接描述符都指向同一个文件表项,父进程关闭它的连接描述符是至关重要的。
- 父进程派生子进程为客户端提供服务,而父进程自己用来等待下一个连接请求。
- 父进程派生一个子进程来处理每个新的连接请求
- 常用函数:
fork
exec
waitpid
- 需要说明的内容:
必须要包括一个SIGCHLD处理程序来回收僵死子进程的资源。
父进程需要关闭它的已连接描述符的拷贝以避免内存泄漏。
父子进程之间共享文件表,但是不共享用户地址空间。
12.1.2 进程的优劣
- 注意:进程的模型:共享文件表,但不是共享用户地址空间。
- 优点:一个进程不可能不小心覆盖两一个进程的虚拟存储器。
- 缺点:独立的地址空间使得进程共享状态信息变得更加困难。进程控制和IPC的开销很高。
- Unix IPC是指所有允许进程和同一台主机上其他进程进行通信的技术,包括管道、先进先出(FIFO)、系统V共享存储器,以及系统V信号量。
12.2 基于I/O多路复用的并发编程
- echo服务器必须响应两个相互独立的I/O时间:
1.网络客户端发起连接请求
2.用户在键盘上键入命令行。
- I/O多路复用技术的基本思路:使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
- echo函数:将来自科幻段的每一行回送回去,直到客户端关闭这个链接。
- 只允许对描述符做的三件事:
1.分配他们。
2.将一个此种类型的变量赋值给另一个变量。
3.用FD_ZERO、FD_SET、FD_CLR和FD_ISSET宏指令来修改和检查它们。
12.2.1 基于I/O多路复用的并发事件驱动服务器
状态机就是一组状态、输入事件和转移,转移就是将状态和输入时间映射到状态,自循环是同一输入和输出状态之间的转移。
- 流程:
select函数检测到输入事件
add_client函数创建新状态机
check_clients函数执行状态转移(在课本的例题中是回送输入行),并且完成时删除该状态机。
- 需要注意的函数:
init_pool:初始化客户端池
add_client:添加一个新的客户端到活动客户端池中
check_clients:回送来自每个准备好的已连接描述符的一个文本行
12.2.2 I/O多路复用技术的优势
- 优点:
(1)比基于进程的设计给了程序员更多的对程序行为的控制
(2)运行在单一进程上下文中,因此,每个逻辑流都能访问该进程的全部地址空间,使得流之间共享数据变得很容易。
(3)不需要进程上下文切换来调度新的流。
- 缺点:
(1)编码复杂
(2)不能充分利用多核处理器
粒度:每个逻辑流每个时间片执行的指令数量。并发粒度就是读一个完整的文本行所需要的指令数量。
12.3 基于线程的并发编程
- 这是前两种创建并发逻辑流方法的混合。
- 简单的说线程就是运行在进程上下文中的逻辑流。
- 线程有自己的线程上下文,包括一个唯一的整数线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有运行在一个进程里的线程共享该进程的整个虚拟地址空间。
12.3.1线程执行模型
- 主线程:每个进程开始生命周期时都是单一线程。
- 对等线程:某一时刻,主线程创建的对等线程
- 线程与进程的不同:
线程的上下文切换要比进程的上下文切换快得多;
和一个进程相关的线程组成一个对等池,独立于其他线程创建的线程。
主线程和其他线程的区别仅在于它总是进程中第一个运行的线程。
- 对等池的影响
一个线程可以杀死它的任何对等线程;
等待它的任意对等线程终止;
每个对等线程都能读写相同的共享资源。
12.3.2 Posix线程
- 线程的代码和本地数据被封装在一个线程例程中。每一个线程例程都以一个通用指针作为输入,并返回一个通用指针。
12.3.3 创建线程
- 通过调用
pthread create
函数创建一个新的线程,并带着一个输入变量arg,在新线程的上下文中运行线程例程f。新线程可以通过调用pthread _self函数来获得自己的线程ID。
#include <pthread.h>
typedef void *(func)(void *);
int pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg);//成功返回0,出错返回非0
#include <pthread.h>
pthread_t pthread_self(void);//查看线程
12.3.4 终止线程
-一个线程的终止方式:
(1)当顶层的线程例程返回时,线程会隐式的终止;
(2)通过调用
pthread _exit
函数,线程会显示地终止。如果主线程调用pthread _exit
,它会等待所有其他对等线程终止,然后再终止主线程和整个进程。
#include <pthread.h>
void pthread_exit(void *thread_return);//从不返回
#include <pthread.h>
void pthread_cancle(pthread_t tid);//若成功返回0,出错为非0
12.3.5 回收已终止线程的资源
- 通过调用
pthread _join
函数等待其他线程终止,回收已终止线程占用的所有存储器资源。pthread _join
函数只能等待一个指定的线程终止。
#include <pthread.h>
int pthread_join(pthread_t tid,void **thrad_return);
12.3.6 分离线程
- 在任何一个时间点上,线程是可结合的,或是分离的。一个可结合的线程能够被其他线程收回其资源和杀死;一个可分离的线程是不能被其他线程回收或杀死的。它的存储器资源在它终止时有系统自动释放。
- 通过调用
pthread _detach
函数被分离。
#include <pthread.h>
void pthread_detach(pthread_t tid);//若成功返回0,出错为非0
12.3.7 初始化线程
- 通过调用
pthread_once
函数初始化与线程例程相关的状态。
#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));//总是返回0
12。4 多线程程序中的共享变量
- 一个变量是共享的,当且仅当多个线程引用这个变量的某个实例。
- 全局变量和static 变量 是存储在数据段,所以,多线程共享之。
- 由于线程的栈是独立的,所有线程中的自动变量是独立的。即使多个线程运行同一段代码总的自动变量,那么他们的值也是根据线程的不同而不同。
- 比如C++中,类属性不是在用户栈中的。所以线程共享之。
12.4.1线程内存模型
- 每个线程和其他线程一起共享进程上下文的剩余部分。包括整个用户虚拟地址空间,是由只读文本、读/写数据、堆以及所有的共享库代码和数据区域组成的。线程也共享同样的打开文件的集合。
- 任何线程都可以访问共享虚拟内存的任意位置。寄存器从不共享,虚拟存储器总是共享的。
12.4.2将变量映射到内存
- 全局变量:全局变量是定义在函数之外的变量。运行时虚拟内存的读/写区域只包含每个全局变量的一个实例,任何线程都可以引用。
- 本地自动变量:本地自动变量是定义在函数内部但没有static属性的变量。运行时每个线程的栈都包含它自己的所有本地自动变量的实例。
- 本地静态变量:本地静态变量是定义在函数内部并有static属性的变量。和全局变量一样运行时虚拟内存的读/写区域只包含每个本地静态变量的一个实例。
12.4.3 共享变量
- 变量v是共享的,当且仅当它的一个实例被一个以上的线程引用。
- 我们说一个变量V是共享的,当且仅当它的一个实例被一个以上的线程引用。例如,示例程序中的变量cnt就是共享的,因为它只有一个运行时实例,并且这个实例被两个对等线程引用在另一方面,myid不是共享的,因为它的两个实例中每一个都只被一个线程引用。然而,认识到像msgs这样的本地自动变量也能被共享是很重要的。
12.5 用信号量同步线程
- 信号量通常称之为PV操作,虽然它的思想是将临界代码保护起来,达到互斥效果。这里面操作系统使用到了线程挂起。
- 共享变量引入了同步错误的可能性。
- 线程i的循环代码分解为五部分:
Hi:在循环头部的指令块
Li:加载共享变量cnt到寄存器%eax的指令,%eax表示线程i中的寄存器%eax的值
Ui:更新(增加)%eax的指令
Si:将%eaxi的更新值存回到共享变量cnt的指令
Ti:循环尾部的指令块。
12.5.1进度图
- 进程图将n个并发进程的执行模型化为一条n维笛卡尔空间中的轨迹线。
- 进度图将指令执行模式化为从一种状态到另一种状态的转换。转换被表示为一条从一点到相邻点的有向边。合法的转换是向右或者向上。
- 互斥的访问:确保每个线程在执行它的临界区中的指令时,拥有对共享变量的互斥的访问。
- 安全轨迹线:绕开不安全区的轨迹线
- 不安全轨迹线:接触到任何不安全区的轨迹线就叫做不安全轨迹线
- 任何安全轨迹线都能正确的更新共享计数器。
12.5.2信号量
- 当有多个线程在等待同一个信号量时,你不能预测V操作要重启哪一个线程。
- 信号量不变性:一个正在运行的程序绝不能进入这样一种状态,也就是一个正确初始化了的信号量有一个负值。
12.5.3 使用信号量来实现互斥
- 二元信号量:将每个共享变量与一个信号量s联系起来,然后用P(S)和V(s)操作将这种临界区包围起来,这种方式来保护共享变量的信号量。
- 互斥锁:以提供互斥为目的的二元信号量
- 加锁:一个互斥锁上执行P操作称为对互斥锁加锁,执行V操作称为对互斥锁解锁。对一个互斥锁加了锁但还没有解锁的线程称为占用了这个互斥锁。
- 计数信号量:一个呗用作一组可用资源的计数器的信号量
12.5.4 利用信号量来调度共享资源
- 信号量的作用:
提供互斥
调度对共享资源的访问
- 生产者—消费者问题:生产者产生项目并把他们插入到一个有限的缓冲区中,消费者从缓冲区中取出这些项目,然后消费它们。
- 读者—写者问题:
读者优先,要求不让读者等待,除非已经把使用对象的权限赋予了一个写者。
写者优先,要求一旦一个写者准备好可以写,它就会尽可能地完成它的写操作。
饥饿就是一个线程无限期地阻塞,无法进展。
12.6 使用线程提高并行性
- 写顺序程序只有一条逻辑流,写并发程序有多条并发流,并行程序是一个运行在多个处理器上的并发程序。并行程序的集合是并发程序集合的真子集。
12.7.1 线程安全
- 我们编程过程中,尽可能编写线程安全函数,即一个函数当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。如果做不到这个条件我们称之为线程不安全函数。下面介绍四类线程不安全函数:
不保护共享变量的函数。解决办法是PV操作。
保持跨越多个调用的状态函数。比如使用静态变量的函数。解决方法是不要使用静态变量或者使用可读静态变量。
返回指向静态变量的指针的函数。解决方法是lock-and-copy(枷锁-拷贝)
调用线程不安全函数的函数
死锁。
- 由于PV操作不当,可能造成死锁现象。这在程序中也会出现。
12.7.4 竞争
- 竞争:当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点时,就会发生竞争。
- 线程化的程序必须对任何可行的轨迹线都正确工作。
12.7.5 死锁
- 一组线程被阻塞了,等待一个永远也不会为真的条件。
- 解决死锁的方法
a.不让死锁发生:
静态策略:设计合适的资源分配算法,不让死锁发生---死锁预防;
动态策略:进程在申请资源时,系统审查是否会产生死锁,若会产生死锁则不分配---死锁避免。
b.让死锁发生:
进程申请资源时不进行限制,系统定期或者不定期检测是否有死锁发生,当检测到时解决死锁----死锁检测与解除。
教材学习中的问题和解决过程
问题1:如何理解线程,主内存和工作内存三者之间的关系
解决:
结对及互评
本周结对学习情况
结对学习内容
第十二章
课后习题
12.1
并发服务器第33行上,父进程关闭了已连接描述符后,子进程仍能够使用该描述符和客户端通信的原因是?
答:当父进程派生子进程时,它得到一个已连接描述符的副本,并将相关文件表中的引用计数从1增加到2.当父进程关闭它的描述符副本时,引用计数就从2减少到1.因为内核不会关闭一个文件,知道文件表中它的引用计数值变为0,所以子进程这边的连接端将保持打开。
12.2
如果我们要删除图12-5中关闭已连接描述符的第30行,从没有内存泄漏的角度来说,代码将仍然是正确的,为什么
答:当一个进程因为某种原因终止时,内核将关闭所有打开的描述符。因此,当子进程退出时,它的已连接文件描述符的副本也将被自动关闭。
12.3
在Linux系统里,在标准输入上键入Ctrl+D表示EOF。图12-6中的程序阻塞在对select的调用上,如果你键入Ctrl+D会发生什么
答:如果一个从描述符中读一个字节的请求不会阻塞,那么这个描述符就准备好可以读了。假如EOF在一个描述符上为真,那么描述符也准备好可读了,因为读操作将立即返回一个零返回码,表示EOF。因此,键入Ctrl+D会导致select函数返回,准备好的集合中有描述符0。
12.4
图12-8所示的服务器中,我们在每次调用select之前都立即小心地重新初始化pool.ready_set变量,为什么?
答:因为变量pool.read_set既作为输入参数,也作为输出参数,所以我们在每一次调用select之前都重新初始化它。在输入时,它包含读集合。在输出时,它包含准备好的集合。
12.5
在下图中基于进程的服务器中,我们在两个位置小心地关闭了已连接描述符:父进程和子进程。然而,在图2中,基于线程的服务器中,我们只在一个位置关闭了已连接描述符:对等线程,为什么?
答:因为线程运行在同一个进程中,它们都共享相同的描述符表。无论有多少线程使用这个已连接描述符,这个已连接描述符的文件表的引用计数都等于1.因此,当我们用完它时,一个close操作就足以释放于这个已连接描述符相关的内存资源了。
12.6
12.7
12.8
指出下列轨迹是否安全。
12.9
12.10
下图所示的对第一类读者-写者问题的解答给予读者较高的优先级,但是从某种意义上说,这种优先级是很弱的,因为一个离开临界区的写者可能重启一个在等待的写者,而不是一个在等待的读者。描述一个场景,其中这种弱优先级会导致一群写者使得一个读者饥饿。
答:假设一个特殊的信号量实现为每一个信号量使用了一个LIFO的线程栈。当一个线程在P操作中阻塞在一个信号量上,它的ID就被压入栈中。类似地,V操作从栈中弹出栈顶的线程ID,并重启这个线程。根据这个栈的实现,一个在它的临界区中竞争的写者会简单的等待,直到在他释放这个信号量之前另一个写者阻塞在这个信号量上。在这种场景中,当两个写者来回地传递控制权时,正在等待的读者可能会永远的等待下去。
12.11
12.12
图中ctime_ts函数是线程安全的,但不是可重入的。请解释说明
答:ctime_ts函数不是可重入函数,因为每次调用都共享相同的由ctime函数返回的static变量。然而,它是线程安全的,因为对共享变量的访问是被P和V操作保护的,因此是互斥的。
12.13
在下图所示代码中,我们可能想要在主线程中的第14行后立即释放已经分配的内存块,而不是在对等线程中释放它,这是个坏主意,为什么?
答:如果在第4行刚调用完pthread_create后就释放内存,这回引起一个竞争,这个竞争发生在主线程对free的调用和24的行的赋值语句之间。
12.14
A.在上图中,我们通过为每个整数ID分配一个独立的块来消除竞争。给出一个不调用malloc或者free函数的不同的方法。
B.这种方法的利弊是什么?
答:
A:另一种方法是直接传递整数i,而不是传递一个指向i的指针
for(i=0;i<N;i++)
Pthread_create(&tid[i],NULL,thread,(void*)i);
我们将参数强制转换成一个int型变量,并将它赋值给myid;
int myid=(int)vargp;
B:优点是它通过消除对malloc和free的调用降低了开销。一个明显的缺点是,它假设指针至少和int一样大。即便这种假设对于所有得现代系统来说都为真,但是它对于那些过去遗留下来的或今后的系统来说可能就不为真了。