2017-2018-1 20155301 《信息安全系统设计基础》第十三周学习总结
本章要点
- 并发:如果逻辑流在时间上重叠,那么他们就是并发的,硬件异常处理程序、进程和UNIX信号处理程序都是熟悉的例子。并发现象不仅在内核中存在,在应用级别的程序中也存在
- 三种基本的构造并发程序的方法:
1)进程。每个逻辑控制流都是一个进程,由内核来调度和维护
2)I/O多路复用
3)线程
- 基于进程的并发 echo 服务器.父进程派生一个子进程来处理每个新的连接请求
- 进程能够共享文件表,但不共享用户地址空间
- 基于I/O多路复用的并发编程:
面对困境——服务器必须响应两个互相独立的I/O事件:
1)网络客户端发起的连接请求
2)用户在键盘上键入的命令 ,解决的办法是I/O多路复用技术。
基本思想是,使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
- select函数:
select函数处理类型为fd_set的集合,即描述符集合,并在逻辑上描述为一个大小为n的位向量,每一位b[k]对应描述符k,但当且仅当b[k]=1,描述符k才表明是描述符集合的一个元素。
使用select函数的过程如下:
第一步,初始化fd_set集,19~22行;
第二步,调用select,25行;
第三步,根据fd_set集合现在的值,判断是哪种I/O事件,26~31行。
基于I/O多路复用的并发事件驱动服务器:I/O多路复用可以用做并发事件驱动程序的基础,在事件驱动程序中,流是因为某种事件而前进的,一般概念是把逻辑流模型化为状态机。一个状态机就是一组状态、输入事件和转移。
并发事件驱动程序中echo服务器中逻辑流的状态机,如下图所示:
- 线程:运行在进程上下文中的逻辑流。线程由内核自动调度,每个线程都有它自己的线程上下文。
- 线程上下文包括:一个唯一的整数线程ID——TID、栈、栈指针、程序计数器、通用目的寄存器、条件码
线程执行模型
创建线程,调用pthread_create函数来创建其他线程,pthread_create函数创建一个新的线程,带着一个输入变量arg,在新线程的上下文运行线程例程f。
attr默认为NULL
# include <pthread.h>
typedef void *(func)(void *);
int pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg);
返回:成功返回0,出错返回非0
- 新线程调用pthread_self函数来获得自己的线程ID
#include <pthread.h>
pthread_t pthread_self(void);
返回调用者的线程ID(TID)
- 终止线程的几个方式:
1)当顶层的线程例程返回时,线程会隐式终止;
2)线程调用pthread_exit函数,线程会显示终止;如果主线程调用pthread_exit,它会等到所有其他对等线程终止,然后再终止主线程和整个线程,返回值为thread_return;
pthread_exit函数
#include <pthread.h>
void pthread_exit(void *thread_return);
若成功返回0,出错为非0
3)某个对等线程调用exut函数,则函数终止进程和所有与该进程相关的线程;
4)另一个对等线程调用以当前ID为参数的函数ptherad_cancel来终止当前线程
#include <pthread.h>
void pthread_cancle(pthread_t tid);
若成功返回0,出错为非0
- 调用pthread_join函数回收已终止线程的资源
这个函数会阻塞,直到线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源
- 分离线程
在任何一个时间点上,线程是可结合的,或是分离的。
一个可结合的线程能够被其他线程回收其资源和杀死,在被其他线程回收之前,它的存储其资源是没有被释放的;相反,一个分离的线程是不能被其他线程回收或杀死的。它的存储器资源是在它终止时系统自动释放的。默认情况下,线程被创建成可结合的。但现实程序中,有很好的理由要使用分离线程。 - 初始化线程:pthread_once函数
- 一个基于线程的并发服务器
- 借助进度图
1.进度图是将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,原点对应于没有任何线程完成一条指令的初始状态。
当n=2时,状态比较简单,是比较熟悉的二维坐标图,横纵坐标各代表一个线程,而转换被表示为有向边
2.进度图将指令执行模型化为从一种状态到另一种状态的转换(transition)。转换被表示为一条从一点到相邻点的有向边。合法的转换是向右(线程 1 中的一条指令完成〉或者向上(线程 2 中的一条指令完成)的。两条指令不能在同一时刻完成一一对角线转换是不允许的。程序决不会反向运行,所以向下或者向左移动的转换也是不合法的。
一个程序的执行历史被模型化为状态空间中的一条轨迹线。
临界区:对于线程i,操作共享变量cnt内容的指令L,U,S构成了一个关于共享变量cnt的临界区。
不安全区:两个临界区的交集形成的状态
安全轨迹线:绕开不安全区的轨迹线,绕开不安全区的轨迹线叫做安全轨迹线 (safe trajectory)。相反,接触到任何不安全区的轨迹线就叫做不安全轨迹线 (unsafe trajectory)。
- 一种解决同步不同执行线程问题的方法,这种方法是基于一种叫做信号量 (semaphore) 的特殊类型变量的。信号量 s 是具有非负整数值的全 局变量,只能由两种特殊的操作来处理,这两种操作称为 P 和 V:
通过调用 sem_wait 和 sem_post 函数来执行P和V操作。
1)P(s):如果s是非零的,那么P将s减1,并且立即返回。如果s为零,那么就挂起这个线程,直到s变为非零,而一个V操作会重启这个线程。在重启之后,P 操作将s减1,并将控制返回给调用者。
2)V(s):V操作将s加1。如果有任何线程阻塞在P 操作等待s变成非零,那么V操作会重启这些线程中的一个,然后该线程将s减1,完成它的P操作。
信号量的函数
P和V的包装函数
使用信号量来实现互斥
基本思想:将每个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P和V操作将相应的临界区包围起来。利用信号量来调度共享资源,信号量有两个作用:
1)实现互斥
2)调度共享资源
- 使用线程提高并行性
顺序、并发和并行程序集合之间的关系
并行程序的加速比 通常定义为,其中p 是处理器核的数量,凡是在 k个核上的运行时间。这个公式有时称为强扩展 (strong scaling)。当 T1 是程序顺序执行版本的执行时间时, Sp 称为绝对加速比.(absolute speedup)。当 T1 是程序并行版本在一个核上的执行时间时, Sp 称为相对加速比 (relative speedup)。绝对加速 比比相对加速比能更真实地衡量并行的好处。
效率被定义如图所示,它通常表示为范围在 (0, 100] 之间的百分比。效率是对由于并行化造成的开销的衡量。具有高 效率的程序比效率低的程序在有用的工作上花费更多的时间,在同步和通信上花费更少的时间。
- 线程安全是指一个函数被称为线程安全的,当且仅当被多个并发线程反复的调用时,它会一直产生正确的结果
可重入函数,其特点在于它们具有这样一种属性:当它们被多个线程调用时,不会引用任何共享数据。
可重入函数通常要比不可重人的线程安全的函数高效一些,因为它们不需要同步操作。更进一步来说,将第2类线程不安全函数转化为线程安全函数的唯一方法就是重写它,使之变为可重入的。
可重入函数分为两类:
1)显式可重入的:所有函数参数都是传值传递,没有指针,并且所有的数据引用都是本地的自动栈变量,没有引用静态或全剧变量。
2)隐式可重入的:调用线程小心的传递指向非共享数据的指针。
- 竞争是由于一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点。通常发生竞争是因为程序员假定线程会按照某种特殊的轨迹穿过执行状态空间,忘了一条准则规定:线程化的程序必须对任何可行的轨迹线都正确工作。
- 消除竞争的方法:动态的为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针。
课下练习
- 12.1 在图12-5中,并发服务器的第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 在图12-5中基于进程的服务器中,我们在两个位置小心地关闭了已连接描述符:父进程和子进程。然而,在图12-14中,基于线程的服务器中,我们只在一个位置关闭了已连接描述符:对等线程,为什么?
答:因为线程运行在同一个进程中,它们都共享相同的描述符表。无论有多少线程使用这个已连接描述符,这个已连接描述符的文件表的引用计数都等于1.因此,当我们用完它时,一个close操作就足以释放于这个已连接描述符相关的内存资源了。
- 12.6
A.利用12.4节中的分析,为图12-15中的示例程序在下表的每个条目中填写“是”或者“否”。在第一列中,符号v,t表示变量v的一个实例,它驻留在线程t的本地栈中,其中t要么是m(主线程),要么是p0(对等线程)或者p1(对等线程1)
答:
B.根据A部分的分析,变量ptr、cnt、i、msgs和myid那些是共享的
答:变量ptr、cnt、msgs被多于一个线程引用,所以它们是共享的。
- 12.7 根据badcnt.c的指令顺序完成下表,这种顺序会产生一个正确的值吗?
答:
变量cnt最终有一个不正确的值1
- 12.8 使用图12-21中的进度图,将下列轨迹线划分为安全的或者不安全的
A.H1,L1,U1,S1,H2,L2,U2,S2,T2,T1
B.H2,L2,H1,L1,U1,S1,T1,U2,S2,T2
C.H1,H2,L2,U2,S2,L1,U1,S1,T1,T2
答:
A.H1,L1,U1,S1,H2,L2,U2,S2,T2,T1是安全的
B.H2,L2,H1,L1,U1,S1,T1,U2,S2,T2是不安全的
C.H1,H2,L2,U2,S2,L1,U1,S1,T1,T2是安全的
- 12.9 设p表示生产者数量,c表示消费者数量,而n表示以项目单元为单位的缓冲区大小。对于下面的每个场景,指出sbuf_insert和sbuf_remove中的互斥锁信号量是否是必需的。
A.p=1,c=1,n>1
B.p=1,c=1,n=1
C.p>1,c>1,n=1
答:
A.互斥锁是需要的,因为生产者和消费者会并发地访问缓冲区
B.不需要互斥锁,因为一个非空的缓冲区就等于满的缓冲区。当缓冲区包含一个项目时,生产者就别阻塞了,当缓冲区为空时,消费者就被阻塞了。所以在任意时刻,只有一个线程可以访问缓冲区,因此不用互斥锁也能保证互斥
C.不需要互斥锁,原因与B相同。
- 12.10 图12-26所示的对第一类读者-写者问题的解答给予读者较高的优先级,但是从某种意义上说,这种优先级是很弱的,因为一个离开临界区的写者可能重启一个在等待的写者,而不是一个在等待的读者。描述一个场景,其中这种弱优先级会导致一群写者使得一个读者饥饿。
答:假设一个特殊的信号量实现为每一个信号量使用了一个LIFO的线程栈。当一个线程在P操作中阻塞在一个信号量上,它的ID就被压入栈中。类似地,V操作从栈中弹出栈顶的线程ID,并重启这个线程。根据这个栈的实现,一个在它的临界区中竞争的写者会简单的等待,直到在他释放这个信号量之前另一个写者阻塞在这个信号量上。在这种场景中,当两个写者来回地传递控制权时,正在等待的读者可能会永远的等待下去。
- 12.11 对于下表中的并行程序,填写空白处。假设使用强扩展。
答:
- 12.12 图12-38中的ctime_ts函数是线程安全的,但不是可重入的,请解释说明。
答:ctime_ts函数不是可重入函数,因为每次调用都共享相同的由ctime函数返回的static变量。然而,它是线程安全的,因为对共享变量的访问是被P和V操作保护的,因此是互斥的。
- 12.13 在图12-43中,我们可能想要在主线程中的第14行后立即释放已分配的内存块,而不是在对等线程中释放它。但是这会是个坏主意,为什么?
答:如果在第14行调用了pthread_create之后,我们立即释放块,那么将引入一个新的竞争,这次竞争发生在主线程对free的调用和线程例程中第24行的赋值语句之间。
- 12.14
A.在图12-43中,我们通过为每个整数ID分配一个独立的块来消除竞争。给出一个不调用malloc或者free函数的不同的方法。
答:另一种方法是直接传递整数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一样大。即便这种假设对于所有得现代系统来说都为真,但是它对于那些过去遗留下来的或今后的系统来说可能就不为真了。
- 12.15 思考下面的程序,它试图使用一对信号量来实现互斥。
A.画出这个程序的进度图。
答:
B.它总是会死锁吗
答:会,因为任何可行的轨迹最终都陷入思索状态。
C.如果是,那么对初始信号量的值做哪些简单的改变就能消除这种潜在的死锁呢?
答:为了消除潜在的死锁,将二元信号量t初始化为1而不是0。
D.画出得到的无死锁程序的进度图。
答:
家庭作业
- 12.16 编写一个hello.c,他创建和回收n个可结合的对等线程,其中n是一个命令行参数。
代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
void *thread(void *vargp);
int main(int argc, char **argv)
{
pthread_t tid;
int i,n;
n = atoi(argv[1]);
for(i=0;i<n;i++)
{
pthread_create(&tid,NULL,thread,NULL);
pthread_join(tid,NULL);
}
exit(0);
}
void *thread(void *vargp)
{
printf("hello world!\\n");
return NULL;
}
- 12.17 运行如下代码
A.并不会输出字符,是为什么?
B.通过是用什么函数来改正这个错误呢?
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
void *thread(void *vargp);
int main(int argc, char **argv)
{
pthread_t tid;
int i,n;
pthread_create(&tid,NULL,thread,NULL);
exit(0);
}
void *thread(void *vargp)
{
sleep(1);
printf("hello world!\\n");
return NULL;
}
答:A.是因为exit(0),使得线程在主线程中结束了,所以没有打印出字符。
B.使用pthread_join就可以解决这个问题12.18 用进度图说明下面的轨迹分类是否安全:
A.H2,L2,U2,H1,L1,S2,U1,S1,T1,T2
B.H2,H1,L1,U1,S1,L2,T1,U2,S2,T2
C.H1,L1,H2,L2,U2,S2,U1,S1,T1,T2
答:结果:A不安全,B安全,C安全
- 12.19 教材p707给出的代码,给予了读者比较弱的优先级,当读者离开缓冲区时,可能会重启一个正在等待的写着,而不是一个正在等待的读者,试着更改代码,使得读者优先。
信号量代码如下:
int readcnt;
sem_t mutex=1,w=1,z=1;
void reader(void)
{
while(1)
{
P(&mutex);
readcnt++;
if(readcnt==1)
P(&w);
V(&mutex);
P(&mutex);
readcnt--;
if(readcnt==0)
V(&w);
V(&mutex);
}
}
void writer(void)
{
while(1)
{
P(&z);
P(&w);
V(&w);
V(&z);
}
}
- 12.22 检查对select函数的理解,修改下图所示服务器使得它在主服务器每次迭代中至多只会送一个文本:
答:在while循环中增加一个FD_AERO(&ready_set);
就可以了。
12.24 RIO I/O包中的函数都是线程安全的,那么他们都是是可重入的吗?
答:是不可重入的,RIO包中有专门的数据结构为每一个文件描述符都分配了相应的独立的读缓冲区,它提供了与系统I/O类似的函数接口,在读取操作时,RIO包加入了读缓冲区,一定程度上增加了程序的读取效率。另外,带缓冲的输入函数是线程安全的,这与Stevens的 UNP 3rd Edition(中文版) P74 中介绍的那个输入函数不同。UNP的那个版本的带缓冲的输入函数的缓冲区是以静态全局变量存在,所以对于多线程来说是不可重入的。12.29 下面的程序会死锁吗?为什么?
初始时:a=1;b=1;c=1
线程1 : 线程2:
p(a); p(c);
p(b); p(b);
v(b); v(b);
p(c); v(c);
v(c);
v(a);
答: 会死锁,因为线程1和2在执行完第一步之后都被挂起,都得不到需要的资源。
教材学习中的问题和解决过程
(一个模板:我看了这一段文字 (引用文字),有这个问题 (提出问题)。 我查了资料,有这些说法(引用说法),根据我的实践,我得到这些经验(描述自己的经验)。 但是我还是不太懂,我的困惑是(说明困惑)。【或者】我反对作者的观点(提出作者的观点,自己的观点,以及理由)。 )
- 问题1:对于教材中的问题关于重入的部分不是很理解。
- 问题1解决方案:上网搜索,知道了什么是重入什么是不重入,所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会 出错。不可重入函数在实时系统设计中被视为不安全函数。
满足下列条件的函数多数是不可重入的:
(1)函数体内使用了静态的数据结构;
(2)函数体内调用了malloc()或者free()函数;
(3)函数体内调用了标准I/O函数。
详细可以参考链接提供的内容。
- 问题2:XXXXXX
- 问题2解决方案:XXXXXX
- ...
代码调试中的问题和解决过程
- 问题1:在线程的编译过程中出现下图所示问题
- 问题1解决方案:忘记了要加-lpthread编译,由于pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,需要链接该库。
代码托管
(statistics.sh脚本的运行结果截图)
上周考试错题总结
- 错题1及原因,理解情况
- 错题2及原因,理解情况
- ...
结对及互评
点评模板:
- 博客中值得学习的或问题:
- xxx
- xxx
- ...
- 代码中值得学习的或问题:
- xxx
- xxx
- ...
- 其他
本周结对学习情况
- 20155301](https://home.cnblogs.com/u/fengxingck/)
- 结对照片
- 结对学习内容
- 共同学习了第12章
其他(感悟、思考等,可选)
xxx
xxx
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 200/200 | 2/2 | 20/20 | |
第二周 | 300/500 | 2/4 | 18/38 | |
第三周 | 500/1000 | 3/7 | 22/60 | |
第四周 | 300/1300 | 2/9 | 30/90 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。
计划学习时间:XX小时
实际学习时间:XX小时
改进情况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)