2017-2018-1 20155307 《信息安全系统设计基础》第十三周学习总结
本周任务:
找出全书你认为最重要的一章,深入重新学习一下,要求(期末占10分):
完成这一章所有习题-详细总结本章要点-给你的结对学习搭档讲解你的总结并获取反馈
教材的第十二章学习
章节大意
1.基于进程的并发:最简单的构造并发程序的方法
2.基于I/O的并发:多路复用解决echo服务器类问题
3.基于线程:基于线程的逻辑流综合了进程和I/O的特点
4.多线程程序的共享变量:线程具有容易共享相同程序的特性
5.用信号量同步线程:尝试解决同步错误问题
6.提高并行性:多核处理器上的并发
7.其他可能出现的问题:线程安全性问题
1.基于进程的并发编程
基于进程的并发编程可以有哪些方法?
进程是构造并发程序最简单的方法。
一个自然地构造报并发程序的方法是在父进程中接受客户端的连接请求,然后创建一个子进程为每个客户端提供服务。
其实就是父进程在不断监听,一旦有客户端发送连接请求,服务器便生成子进程1来为其服务,此时必须关闭父进程的已连接描述符,以及子进程的监听描述符,当再有客户端发来请求时再根据同样的步骤生成子进程2来为其服务,这样服务器就可以同时为多个客户端服务。父子进程必须关闭它们各自的connfd拷贝。父进程必须关闭它的已连接描述符,以避免存储器泄漏。直到父子进程的connfd都关闭了,到客户端的连接才会终止。
父、子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。
使用显式的进程间通信(IPC)机制。但开销很高,往往比较慢。
基于I/O多路复用的并发编程
- 当我们需要写一个也能对用户从标准输入键入的交互式命令作出响应的服务器时,我们需要使用I/O多路复用并发编程。因为这种时候我们既需要响应客户端发起的链接请求,也需要对用户键入的命令行作出反应。
以下这些技术是I/O多路复用所必须的:I/O 多路复用(I/O multiplexing)技术、函数原型、select函数处理类型为fd_set的集合。
对描述符集合的处理方法只能有以下三种:
分配出去-额外复制-使用宏指令来修改或检查。select函数:若一直处于阻塞状态,直到由用户在键盘输入并且敲下回车或者有客户端发来连接请求才会调用不同的函数来执行响应,如果是用户在键盘输入并且敲下回车则调用command函数,如果是客户端发来连接请求则调用accept函数来执行响应。
但是存在一个问题就是一旦被一个客户端占用,标准输入就很久不会得到响应,解决的方法是更细粒度的多路复用。
将逻辑流模型化为状态机。一个状态机从某种初始状态开始执行。每个输入事件都会引发一个从当前状态到下一状态的转移。
优点:比基于进程的设计给了程序员更多的对进程行为的控。一个基于 I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间。
缺点:编码复杂。
基于线程的并发编程
线程是以上两种方法的混合,线程是运行在进程上下文中的逻辑流。每个进程开始生命周期时都是单一线程,称为主线程,在某一时刻创建一个对等线程,从此开始并发地运行,最后,因为主线程执行一个慢速系统调用,或者被中断,控制就会通过上下文切换传递到对等线程。线程上下文切换,要比进程的上下文切换快得多,一个线程可以杀死或者等待他的任意对等线程。
创建线程的方法:通过调用pthread_create来创建其他线程。函数原型:int pthread_create(pthread_t tid,pthread_attr_t attr,func f,void arg); 成功则返回0,出错则为非零
pthread_ create 函数创建一个新的线程,并带着一个输入变量arg,在新线程的上下文中运行线程例程f。用attr参数来改变新创建线程的默认属性,我们可以将其设为NULL,返回时,参数tid包含新创建线程的ID。新线程可以通过调用 pthreadself 函数来获得它自己的线程 ID: pthread_t pthread_self(void);返回调用者的线程ID。# include <pthread.h> typedef void *(func)(void *); int pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg);
终止线程
一个线程是通过以下方式之一来终止的:
1.顶层的线程例程返回时,线程会隐式地终止。
2.通过调用 pthreadexit 函数,线程会显式地终止。如果主线程调用 pthreadexit , 它会等待所有其他对等线程终止,然后再终止主线程和整个进程,返回值为 thread_return: void pthread_exit(void *thread_return);
#include <pthread.h>
void pthread_exit(void *thread_return);
与wait函数不同,pthread_join函数只能等待一个指定的线程终止。
分离线程
在任何一个时间点上,线程是可结合或可分离的。一个可结合的线程能够被其他线程收回其资源和杀死,在被回收之前,它的内存资源是没有被释放的。相反,分离的线程不能被其他回收或杀死的,它的内存资源在其终止时自动释放。
默认情况下,线程被创建成可结合的,为了避免内存泄漏,每个可结合的线程都应该要么被其他进程显式的回收,要么通过调用pthread _detach函数被分离。
函数原型:int pthread_deacth(pthread_t tid); 成功则返回0,出错则为非零#include <pthread.h> void pthread_cancle(pthread_t tid);
用信号量同步线程
共享变量是十分方便,但是它们也引入了同步错误上网可能性,进度图是将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,原点对应于没有任何线程完成一条指令的初始状态,进度条将指令模型化为从一种状态到另一种状态的转换。
- 信号量
P(s):如果s是非零的,那么P将s减一,并且立即返回。如果s为零,那么就挂起这个线程,直到s变为非零。一个V操作会重启这个线程。
V(s):将s加一,如果有任何线程阻塞在P操作等待s变为非零,那么V操作会重启线程中的一个,然后该线程将s减一,完成他的P操作。 - 执行P和V操作:
int sem_wait(sem_t *s);//P(s) int sem_post(sem_t *s);//V(s)
- 使用信号量来实现互斥
信号量提供了一种很方便的方法来确保对共享变量的互斥访问:将每个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P(s)和V(s)操作将相应的临界区包围起来。
禁止区:s<0,因为信号量的不变性,没有实际可行的轨迹线能够包含禁止区中的状态。 读者—写者问题
读者优先,要求不让读者等待,除非已经把使用对象的权限赋予了一个写者。
写者优先,要求一旦一个写者准备好可以写,它就会尽可能地完成它的写操作。
其他并发问题
- 线程安全
一个线程是安全的,当且仅当被多个并发线程反复的调用时,它会一直产生正确的结果。
四个不相交的线程不安全函数类:
1.不保护共享变量的函数。
2.保持跨越多个调用的状态的函数。
3.返回指向静态变量的指针的函数。
4.调用线程不安全函数的函数 可重入性
可重入函数分为两类:
显式可重入的:所有函数参数都是传值传递,没有指针,并且所有的数据引用都是本地的自动栈变量,没有引用静态或全剧变量。
隐式可重入的:调用线程小心的传递指向非共享数据的指针。竞争是由于一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点。通常发生竞争是因为程序员假定线程会按照某种特殊的轨迹穿过执行状态空间,忘了一条准则规定:线程化的程序必须对任何可行的轨迹线都正确工作。
消除竞争的方法:动态的为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针。
死锁
死锁是不可预测的,如果对于程序中每对互斥锁(s,t),给所有的锁分配一个全序,每个线程按照这个顺序来请求锁,并且按照逆序来释放,这个程序就是无死锁的。
课后习题解答
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 在图12-5中基于进程的服务器中,我们在两个位置小心地关闭了已连接描述符:父进程和子进程。然而,在图12-14中,基于线程的服务器中,我们只在一个位置关闭了已连接描述符:对等线程,为什么?
图片见12.1
答:因为线程运行在同一个进程中,它们都共享相同的描述符表。无论有多少线程使用这个已连接描述符,这个已连接描述符的文件表的引用计数都等于1.因此,当我们用完它时,一个close操作就足以释放于这个已连接描述符相关的内存资源了。
12.6 根据下图所示代码,完成下表,其中符号v.t表示变量v的第t个实例,m是主线程,p0对等线程0,p1对等线程1,并且指出ptr,cnt,i,msgs,myid哪些是共享变量。
12.7 根据badcnt.c的指令顺序完成下表参考下图所示步骤完成了本表格
12.8 指出下列轨迹是否安全。
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、C安全,B不安全。轨迹如下图所示
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下图所示的对第一类读者-写者问题的解答给予读者较高的优先级,但是从某种意义上说,这种优先级是很弱的,因为一个离开临界区的写者可能重启一个在等待的写者,而不是一个在等待的读者。描述一个场景,其中这种弱优先级会导致一群写者使得一个读者饥饿。
答:当一个线程在P操作中阻塞在一个信号量上,它的ID就被压入栈中,类似地,V操作从栈中弹出栈顶的线程ID,并重启这个线程,根据这个栈的实现,一个在它的临界区中竞争的写者会简单的等待,直到在他释放这个信号量之前另一个写者阻塞在这个信号量上,在这种场景中,当两个写者来回地传递控制权时,正在等待的读者可能会永远的等待下去。
12.13 在下图所示代码中,我们可能想要在主线程中的第14行后立即释放已经分配的内存块,而不是在对等线程中释放它,这是个坏主意,为什么?
答:如果在第4行刚调用完pthread_create后就释放内存,这回引起一个竞争,这个竞争发生在主线程对free的调用和24的行的赋值语句之间。
12.16 编写一个hello.c,他创建和回收n个可结合的对等线程,其中n是一个命令行参数。
#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),使得线程在主线程中结束了,所以没有打印出字符。
教材学习中的问题和解决过程
问题1:对于教材中的问题中关于互斥锁加锁顺序具体规则是什么不太理解。
问题1解决方案:上网搜索,知道了如果对于程序中每对互斥锁(s,t),每个同时占用s和t的线程都按照相同的顺序对它们加锁,那么这个程序就是无死锁的。给定所有的胡扯操作的一个全序,如果每个线程都是以一种顺序获得胡扯所兵以相反的顺序释放,那么这个程序就是无死锁的。
结对及互评
本周结对学习情况
- 20155338
- 结对照片
- 结对学习内容
- 共同学习了第12章和第
其他(感悟、思考等,可选)
我认为12章内容作为并发编程,在提升速度的同时也带来了风险,所以需要我们在注重避免风险的同时,尽力提升速度,优化客户体验。
- 共同学习了第12章和第
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 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 | |
第五周 | 400/1700 | 2/11 | 30/90 | |
第六周 | 300/2000 | 3/14 | 30/90 | |
第七周 | 200/2200 | 3/17 | 15/105 | |
第八周 | 300/2500 | 1/18 | 17/122 | |
第九周 | 300/2800 | 3/21 | 15/137 | |
第十周 | 200/3000 | 3/24 | 12/159 | |
第十一周 | 300/3300 | 2/26 | 11/170 | |
第十二周 | 200/3500 | 1/27 | 10/180 | |
第十三周 | 100/3600 | 2/29 | 7/187 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。