信息安全系统设计基础第十二周学习总结—20135227黄晓妍
第十二章 并发编程
操作系统提供了三种基本的构造并发程序的方法:
1、进程。每个逻辑控制流都是一个进程,由内核来调度和维护;
2、I/O多路复用。
3、线程。
一、基于进程的并发编程
在接受连接请求之后,服务器派生出一个子进程,这个子进程获得服务器描述表完整的拷贝。子进程关闭它的拷贝中监听描述符3,父进程关闭它的已连接描述符4的拷贝,因为不需要这些描述符了。
程序实例:
因为通常服务器会运行很长时间,所以需要一个SIGCHLD处理程序,来回收僵死进程。因为当SIGCHLD执行时,信号是阻塞的,而UNIX信号是不排队的,所以SIGCHLD必须准备好回收多个僵死进程。另外注意,循环中的父进程和子进程关闭各自需要关闭的描述符。
进程能够共享文件表,但不共享用户地址空间。
二、基于I/O多路复用的并发编程
1、面对困境——服务器必须响应两个互相独立的I/O事件:1)网络客户端发起的连接请求 2)用户在键盘上键入的命令 ,解决的办法是I/O多路复用技术。基本思想是,使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
select函数如下:
程序示例如下:
使用select函数的过程如下:
第一步,初始化fd_set集,19~22行;
第二步,调用select,25行;
第三步,根据fd_set集合现在的值,判断是哪种I/O事件,26~31行。
2、基于I/O多路复用的并发事件驱动服务器
I/O多路复用可以用做并发事件驱动程序的基础,在事件驱动程序中,流是因为某种事件而前进的,一般概念是把逻辑流模型化为状态机。一个状态机就是一组状态、输入事件和转移。
并发事件驱动程序中echo服务器中逻辑流的状态机,如下图所示:
三、基于线程的并发编程
1、线程运行在进程上下文中的逻辑流。线程由内核自动调度,每个线程都有它自己的线程上下文。
2、线程执行模型。多线程的执行模型在某些方面和多进程的执行模型相似。每个进程开始生命周期时都是单一线程,这个线程称为主线程。在某一时刻,主线程创建一个对等线程,从在此刻开始,两个线程就并发地运行。
3、Posix线程
创建线程:
获取自身ID:
终止线程:
有以下四种方式终止线程:
当顶层的线程例程返回时,线程会隐式终止;
线程调用pthread_exit函数,线程会显示终止;如果主线程调用pthread_exit,它会等到所有其他对等线程终止,然后再终止主线程和整个线程,返回值为thread_return;
某个对等线程调用exut函数,则函数终止进程和所有与该进程相关的线程;
另一个对等线程调用以当前ID为参数的函数ptherad_cancel来终止当前线程。
回收已终止线程的资源:
pthread_join函数会终止,直到线程tid终止。和wait不同,该函数只能回收指定id的线程,不能回收任意线程。
分离线程:
一个可结合的线程能够被其他线程回收其资源和杀死,在被其他线程回收之前,它的存储其资源是没有被释放的;相反,一个分离的线程是不能被其他线程回收或杀死的。它的存储器资源是在它终止时系统自动释放的。默认情况下,线程被创建成可结合的。但现实程序中,有很好的理由要使用分离线程。
初始化线程:该函数用来初始化多个线程共享的全局变量。
一个基于线程的并发服务器:
以上程序可能会出错,因为在对等线程的赋值语句和主线程的accept的语句见引入了竞争——如果赋值语句在下一个accept之前完成,则不会出错;如果赋值语句是在accept之后完成,那么对等线程的局部变量connfd就得到下一次连接的描述符。解决办法是,必须将每个accept返回的描述符分配到它自己的动态分配的存储器块。(21~23行) 32行:动态内存空间释放,释放那个指向动态内存的指针即可,不一定非要是malloc当时生成的指针。
4、多线程程序中的共享变量
每个线程都有它自己独自的线程上下文,包括线程ID、栈、栈指针、程序计数器、条件码和通用目的寄存器值。每个线程和其他线程一起共享进程上下文的剩余部分。寄存器是从不共享的,而虚拟存储器总是共享的。线程化的c程序中变量根据它们的存储器类型被映射到虚拟存储器:全局变量,本地自动变量(不共享),本地静态变量。
5、用信号量同步线程
共享变量引入了同步错误。
进度图:
轨迹线示例:
临界区(不安全区):
信号量:是用信号量解决同步问题,信号量s是具有非负整数值的全局变量,有两种特殊的操作来处理(P和V):
P(s):如果s非零,那么P将s减1,并且立即返回。如果s为0,那么就挂起这个线程,直到s变为非零;
V(s):V操作将s加1。
使用信号量实现互斥:
利用信号量调度共享资源:在这种场景中,一个线程用信号量操作来通知另一个线程,程序状态中的某个条件已经为真了。两个经典应用:
a)生产者——消费者问题
要求:必须保证对缓冲区的访问是互斥的;还需要调度对缓冲区的访问,即,如果缓冲区是满的(没有空的槽位),那么生产者必须等待直到有一个空的槽位为止,如果缓冲区是空的(即没有可取的项目),那么消费者必须等待直到有一个项目变为可用。
注释:5~13行,缓冲区初始化,主要是对缓冲区结构体进行相关操作;16~19行,释放缓冲区存储空间;22~29行,生产(有空槽的话,在空槽中插入内容);32~4行,消费(去除某个槽中的内容,使该槽为空)
b)读者——写者问题
修改对象的线程叫做写者;只读对象的线程叫做读者。写着必须拥有对对象的独占访问,而读者可以和无限多个其他读者共享对象。读者——写者问题基本分为两类:第一类,读者优先,要求不要让读者等待,除非已经把使用对象的权限赋予了一个写者。换句话说,读者不会因为有一个写者等待而等待;第二类,写者优先,要求一定能写者准备好可以写,它就会尽可能地完成它的写操作。同第一类问题不同,在一个写者后到达的读者必须等待,即使这个写者也是在等待。以下程序给出了第一类读者——写者问题的解答:
注释:信号量w控制对访问共享对象的临界区的访问。信号量mutex保护对共享变量readcnt的访问,readcnt统计当前临界区的读者数量。每当一个写者进入临界区,它就对互斥锁w加锁,每当它离开临界区时,对w解锁,这就保证了任意时刻临界区最多有一个写者;另一方面,只有第一个进入临界区的读者对w加锁,而只有最后一个离开临界区的读者对w解锁。
综合:基于预线程的并发服务器之前介绍的基于线程的并发服务器,需要为每个客户端新建一个新线程,导致不小的代价。一个基于预线程化的服务器通过使用如下图所示的生产者——消费者模型来降低这种开销。服务器是由一个主线程和一组工作组线程构成的。主线程不断地接受来自客户端的连接请求,并将得到的连接描述符放在一个有限缓冲区中。每一个工作组线程反复地从共享缓冲区中取出描述符,为客户端服务,然后等待下一个描述符。
程序示例如下图:
注释:26~27行,产生工作组线程;29~32行,接受客户端的连接请求,并把这些描述符放到缓冲区;35~43行,每个线程所要完成的工作;19行,初始化线程共享的全局变量。初始化有两种方式,一种是它要求主线程显示地调用一个初始化函数;第二种是,在此显示的,当第一次有某个线程调用echo_cnt函数时,使用pthread_once函数去调用初始化函数。
6、其他并发问题
a)四种不安全函数;
b)可重入函数。可重入函数是线程安全函数的一个真子集,它不访问任何共享数据。可重入安全函数通常比不可重入函数更有效,因为它们不需要任何同步原语。
c)竞争。当程序员错误地假设逻辑流该如何调度时,就会发生竞争。为了消除竞争,通常我们会动态地分配内存空间。
d)死锁。当一个流等待一个永远不会发生的事件时,就会发生死锁。