2017-2018-1 20155301 《信息安全系统设计基础》第十三周学习总结

2017-2018-1 20155301 《信息安全系统设计基础》第十三周学习总结

本章要点

  • 并发:若是逻辑流在时间上重叠,那么他们就是并发的,硬件异常处理程序、进程和UNIX信号处理程序都是熟悉的例子。并发现象不只在内核中存在,在应用级别的程序中也存在
  • 三种基本的构造并发程序的方法:html

    1)进程。每一个逻辑控制流都是一个进程,由内核来调度和维护git

    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 对于下表中的并行程序,填写空白处。假设使用强扩展。image

  • 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 思考下面的程序,它试图使用一对信号量来实现互斥。
    image
    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小时

  • 改进状况:

(有空多看看现代软件工程 课件
软件工程师能力自我评价表
)

参考资料

相关文章
相关标签/搜索