2017-2018-1 20155318 《信息安全系统设计基础》第十三周学习总结
-
完成这一章所有习题
-
详细总结本章要点
-
给你的结对学习搭档讲解你的总结并获取反馈
-
我选择教材第十二章的内容来深入学习,原因有二,一方面,并发常出现在计算机系统的许多不同层面上,使用非常广泛;另一方面,利用这个知识背景的硬件异常处理程序、Linux信号处理程序非常常见,借此机会来重新学习这一章的内容,加深理解。
教材学习内容总结
第十二章:并发编程
- 并发
- 并发程序:使用应用级并发的应用程序。
- 三种基本构造方法:
- 进程
- I/O多路复用
- 线程
- 基于进程的并发编程
-
构造并发程序最简单的方法就是用进程。在父进程中接受客户端连接请求后,创建一个新的子进程来为每个新客户端提供服务。具体流程如下:
step1.服务器接收客户端的连接请求 step2.服务器派生一个子进程为这个客户端服务 step3.服务器接收另一个连接请求 step4.服务器派生另一个子进程为新的客户端服务
- 基于进程的并发服务器:服务器会运行较长时间,需用SIGCHLD 处理程序回收僵死 (zombie) 子进程的资源
- 当 SIGCHLD 处理程序执行时, SIGCHLD 信号是阻塞的,而 Unix 信号是不排队的。
- 父子进程必须关闭它们各自的 connfd 拷贝。父进程必须关闭它的已连接描述符,以避免存储器泄漏。直到父子进程的 connfd 都关闭了,到客户端的连接才会终止。
- 父、子进程间共享状态信息,但是不共享用户地址空间。
-
进程的优劣
优点:一个进程不可能不小心覆盖另一个进程的虚拟存储器
缺点:独立的地址空间使得进程共享状态信息变得更加困难。 -
基于I/O多路复用的并发编程
-
面对困境——服务器必须响应两个互相独立的I/O事件:
- 网络客户端发起的连接请求
- 用户在键盘上键入的命令
-
解决的办法是I/O多路复用技术。基本思想是,使用```select``函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
-
I/O多路复用技术的优劣
-
优点:
- 使用事件驱动编程,这样比基于进程的设计给了程序更多的对程序行为的控制。
- 一个基于I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都访问该进程的全部地址空间。这使得在流之间共享数据变得很容易。一个与作为单进程运行相关的优点是,你可以利用熟悉的调试工具,例如GDB来调试你的并发服务器,就像对顺序程序那样。最后,事件驱动设计常常比基于进程的设计要高效很多,因为它们不需要进程上下文切换来调度新的流。
-
缺点:
-
事件驱动设计的一个明星的缺点就是编码复杂。我们的事件驱动的并发服务器需要比基于进程的多三倍。不幸的是,随着并发粒度的减小,复杂性还会上升。这里的粒度是指每个逻辑流每个时间片执行的指令数量。
-
基于事件的设计的另一重大的缺点是它们不能充分利用多核处理器。
-
线程
-
线程是运行在进程上下文中的逻辑流每个线程都有它自己的线程上下文 ,包括一个唯一的整数线程、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间
-
主线程:每个进程开始生命周期时都是单一线程,在某一时刻,主线程创建一个对等线程 ,从这个时间点开始,两个线程就并发地运行。最后,因为主线程执行一个慢速系统调用。或者因为它被系统的间隔计时器中断, 控制就会通过上下文切换传递到对等线程。对等线程会执行一段时间,然后控制传递回主线程,依次类推。
线程的上下文切换要比进程的上下文切换快得多。 不是按照严格的父子层次来组织的。 和一个进程相关的线程组成一个对等(线程)池 (pool),独立于其他线程创建的线程。 主线程和其他线程的区别仅在于它总是进程中第一个运行的线程。 对等 (线程)池概念的主要影响是,一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止。 每个对等线程都能读写相同的共享数据
-
创建线程:
pthread_create
函数,一个输入变量arg,能用attr参数来改变新创建线程的默认属性。
当
pthread_create
返回时,参数tid包含新创建线程的ID。新线程可以通过调用``pthread_self```函数来获得它自己的线程ID
-
终止线程
- 通过调用
pthreadexit
函数,线程会显式地终止。如果主线程调用pthreadexit
它会等待所有其他对等线程终止,然后再终止主线程和整个进程,返回值为thread_return
- 另一个对等线程调用以当前ID为参数的函数```ptherad_cancel``来终止当前线程
- 通过调用
-
回收已终止的线程:线程通过调用
pthread_join
函数等待其他线程终止,pthreadjoin
函数会阻塞,直到线程tid
终止,将线程例程返回的 (void*) 指针赋值为threadreturn
指向的位置,然后回收己终止线程占用的所有存储器资源。 -
分离线程:
pthread_detach
函数分离可结合线程tid。线程能够通过以pthread_self()
为参数的pthread_detach
调用来分离它们自己
-
初始化线程:
pthread_once
函数
- 一个基于线程的并发服务器
- 调用
pthread_ create
时,如何将已连接描述符传递给对等线程 - 借助结构体可以把所有的函数化成万能函数的等价形式。
void * func( void * parameter) typedef void* (*uf)(void * para)
fflush(stdout)
:在printf()后使用fflush(stdout)
将要输出的内容输出。
当使用printf()函数后,系统将内容存入输出缓冲区,等到时间片轮转到系统的输出程序时,将其输出。pthread_join()
函数:以阻塞的方式等待thread指定的线程结束。
- 多线程程序中的共享变量
- 线程存储器模型
- 将变量映射到存储器,可分为:
- 全局变量:虚拟存储器的读/写区域只会包含每个全局变量的一个实例。
- 本地自动变量:定义在函数内部但没有static属性的变量。
- 本地静态变量:定义在函数内部并有static属性的变量。
- 共享变量
变量是共享的当且仅当它的一个实例被一个以上的线程引用
- 用信号量同步线程
-
进度图:将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,原点对应于没有任何线程完成一条指令的初始状态。
转换规则: 合法的转换是向右或者向上,即某一个线程中的一条指令完成 两条指令不能在同一时刻完成,即不允许出现对角线 程序不能反向运行,即不能出现向下或向左 线程循环代码的分解: H:在循环头部的指令块 L:加载共享变量cnt到线程i中寄存器%eax的指令 U:更新(增加)%eax的指令 S:将%eax的更新值存回到共享变量cnt的指令 T:循环尾部的指令块 临界区:对于线程i,操作共享变量cnt内容的指令L,U,S构成了一个关于共享变量cnt的临界区。
-
信号量:定义如下,
type semaphore=record count: integer; queue: list of process end; var s:semaphore;
- 使用信号量来实现互斥
- 基本思想:将每个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P和V操作将相应的临界区包围起来。
- 生产者——消费者问题
- 必须保证对缓冲区的访问是互斥的;还需要调度对缓冲区的访问,即,如果缓冲区是满的(没有空的槽位),那么生产者必须等待直到有一个空的槽位为止,如果缓冲区是空的(即没有可取的项目),那么消费者必须等待直到有一个项目变为可用
- 读者—写者问题:
- 读者优先,要求不让读者等待,除非已经把使用对象的权限赋予了一个写者。
- 写者优先,要求一旦一个写者准备好可以写,它就会尽可能地完成它的写操作。
- 饥饿是一个线程无限期地阻塞,无法进展。
- 其他并发问题
- 线程安全:一个线程是安全的,当且仅当被多个并发线程反复的调用时,它会一直产生正确的结果。
- 可重入性
- 显式可重入的:所有函数参数都是传值传递,没有指针,并且所有的数据引用都是本地的自动栈变量,没有引用静态或全剧变量。
- 隐式可重入的:调用线程小心的传递指向非共享数据的指针。
- 在线程化的程序中使用已存在的库函数:线程不安全函数的可重入版本,名字以_r为后缀结尾。
- 竞争
- 发生的原因:一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点。也就是说,程序员假定线程会按照某种特殊的轨迹穿过执行状态空间,忘了 一条准则规定:线程化的程序必须对任何可行的轨迹线都正确工作。
- 消除方法:动态的为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针
- 死锁:一组线程被阻塞了,等待一个永远也不会为真的条件。死锁是不可预测的。
教材习题学习
- 12.1
- 父进程关闭了已连接描述符后,子进程仍能够使用该描述符和客户端通信的原因是?
- 解答:当父进程派生子进程是,它得到一个已连接描述符的副本,并将相关文件表中的引用计数从1增加到2.当父进程关闭他的描述符副本时,引用计数从2减少到1.因为内核不会关闭一个文件,直到文件表中他的应用计数值变为0,所以子进程这边的连接端将保持打开
- 12.3
- 在Linux系统里,在标准输入上键入Ctrl+D表示EOF,若阻塞发生在对select的调用上,键入Ctrl+D会发生什么?
- 解答:会导致select函数但会,准备好的集合中有描述符0
- 12.4
- 在服务器中,每次使用select前都初始化pool.ready_set变量的原因?
- 解答:因为pool.ready_set即作为输入参数也作为输出参数,所以在每一次调用select前都重新初始化他。输入时,他包含读集合,在输出,它包含准备好的集合
- 12.5
- 在基于进程的服务器中两次关闭已连接描述符:父进程和子进程;在基于线程的服务器中,自在一个位置关闭已连接描述符:对等线程,为什么?
- 解答:
- 12.6
- 填写表格
变量实例 | 被主线程引用? | 被对等线程0引用? | 被对等线程1引用? |
---|---|---|---|
ptr | 是 | 是 | 是 |
cnt | 否 | 是 | 是 |
i.m | 是 | 否 | 否 |
msgs.m | 是 | 是 | 是 |
myid.p0 | 否 | 是 | 否 |
myid.p1 | 否 | 否 | 是 |
-
说明:
-
ptr:一个被主线程写和被对等线程度的全局变量
-
cnt:一个静态变量,在内存中被两个对等进程读和写
-
i.m:一个存储在主线程栈中的本地自动变量,不共享。
-
12.7
-
根据badcnt.c的指令顺序填写表格
步骤 | 线程 | 指令 | %eax1 | %eax2 | cnt |
---|---|---|---|---|---|
1 | 1 | H1 | - | - | 0 |
2 | 1 | L1 | 0 | - | 0 |
3 | 2 | H2 | - | - | 0 |
4 | 2 | L2 | - | 0 | 0 |
5 | 2 | U2 | - | 1 | 0 |
6 | 2 | S2 | - | 1 | 1 |
7 | 1 | U1 | 1 | - | 1 |
8 | 1 | S1 | 1 | - | 1 |
9 | 1 | T1 | 1 | - | 1 |
10 | 2 | T2 | - | 1 | 1 |
- 12.16
- 编写hello.c一个版本,创建和回收n个可结合的对等线程,其中n是一个命令行参数
- 代码如下:
#include <stdio.h>
#include "csapp.h"
void *thread(void *vargp);
#define DEFAULT 4
int main(int argc, char* argv[]) {
int N;
if (argc > 2)
unix_error("too many param");
else if (argc == 2)
N = atoi(argv[1]);
else
N = DEFAULT;
int i;
pthread_t tid;
for (i = 0; i < N; i++) {
Pthread_create(&tid, NULL, thread, NULL);
}
Pthread_exit(NULL);
}
void *thread(void *vargp) {
printf("Hello, world
");
return NULL;
}
- 12.17
- 修改程序的bug,要求程序睡眠
1秒钟,然后输出一个字符串 - 代码如下:
#include "csapp.h"
void *thread(void *vargp);
int main()
{
pthread_t tid;
Pthread_create(&tid, NULL, thread, NULL);
// exit(0);
Pthread_exit(NULL);
}
/* Thread routine */
void *thread(void *vargp)
{
Sleep(1);
printf("Hello, world!
");
return NULL;
}
代码托管
上周考试错题总结
结对及互评
本周结对学习情况
- 20155227
- 同伴重点学习了第九章的教材内容,我也重新回顾了那一章的知识重点。
其他(感悟、思考等)
课程的学习接近尾声,老师布置的精学自认为最重要的一章的任务,让我发现了自己在之前学习中的许多疏漏和不足,这还是自认为最重要的一章,其他章节可能有更多需要改进的地方,希望通过这周的学习认识到细致、严谨在学习中的重要性。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 0/0 | 1/1 | 3/3 | |
第二周 | 100/100 | 1/2 | 3/6 | |
第三周 | 300/400 | 1/3 | 4/10 | |
第四周 | 0/400 | 2/5 | 2/12 | |
第五周 | 25/425 | 1/6 | 4/16 | |
第六周 | 181/606 | 3/9 | 10/26 | |
第七周 | 201/807 | 2/11 | 7/33 | |
第八周 | -(包括脚本无意义)/6719 | 2/13 | 7/40 | |
第九周 | 396/7115 | 3/16 | 4/44 | |
第十周 | 1160/8275 | 2/18 | 4/49 | |
第十一周 | 135/8410 | 3/21 | 5/54 | |
第十三周 | 317/8727 | 1/22 | 8/62 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。
-
计划学习时间:5小时
-
实际学习时间:8小时
-
改进情况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)