20175306《信息安全系统设计基础》第十三周学习总结

20175306《信息安全系统设计基础》第十三周学习总结


找出全书你认为最重要的一章,深入重新学习一下,要求(期末占10分):

  1. 完成这一章所有习题
  2. 详细总结本章要点
  3. 给你的结对学习搭档讲解你的总结并获取反馈
  4. 参考上面的学习总结模板,把学习过程通过博客(随笔)发表,博客标题“学号 《信息安全系统设计基础》第十三周学习总结”,博客(随笔)要通过作业提交,截至时间本周日 23:59。

    第十二章:并发编程 学习总结

  • 如果逻辑控制流在时间上重叠,那么他们就是并发的。并发出现在计算机系统的许多不同层面上。
  • 现代操作系统提供了三种基本的构造并发程序的办法
    1.进程:每个逻辑控制流都是一个进程
    2.I/O多路复用:在这种形式的并发编程中,应用程序在一个进程的上下文中显示地调度他们的逻辑流
    3.线程:线程是运行在一个单一进程上下文中的逻辑流。
  • 应用级并发是很有用的:
    1.访问慢速I/O设备。当一个应用正在等待来自慢速 I/O 设备(例如磁盘)的数据到达时, 内核会运行其他进程,使 CPU保持繁忙。每个应用都可以按照类似的方式,通过交替执行 I/O 请求和其他有用的工作来使用并发。
    2.与人交互。和计算机交互的人要求计算机有同时执行多个任务的能力。例如,他们在打印一个文档时,可能想要调整一个窗口的大小。现代视窗系统利用并发来提供这种能力。每次用户请求某种操作(比如通过单击鼠标)时,一个独立的并发逻辑流被创建来执行这个操作。
    3.通过推迟工作以降低延迟。有时,应用程序能够通过推迟其他操作和并发地执行它们,利用并发来降低某些操作的延迟。比如,一个动态存储分配器可以通过推迟合并,把它放到一个运行在较低优先级上的并发"合并"流中,在有空闲的 CPU 周期时充分利用这些空闲 周期,从而降低单个 free 操作的延迟。
    4.服务多个网络客户端。
    5.在多核机器上进行并行计算。许多现代系统都配备有多核处理器,多核处理器中包含多个 CPU。被划分成并发流的应用程序通常在多核机器上比在单处理器机器上运行得快,因为这些流会并行执行,而不是交错执行。

12.1 基于进程的并发编程

基于进程的并发服务器

  • 使用SIGCHLD处理程序来回收僵死子进程的资源。
  • 父进程必须关闭他们各自的connfd拷贝(已连接的描述符),避免存储器泄露。
  • 因为套接字的文件表表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。

    关于进程的优劣

  • 在父子进程之间共享状态信息,通过共享文件表,但是不共享用户地址空间。
  • 使用显式的进程间通信(IPC)机制。但开销很高,往往比较慢。

    基于I/O多路复用的并发编程

  • 使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
    int select(int n,fd_set *fdset,NULL,NULL,NULL); 返回已经准备好的描述符的非0的个数,若出错则为-1。
  • select函数处理类型为fd_set的集合,叫做描述符集合,看做一个大小为n位的向量:bn-1,......,b1,b0
  • 对描述符集合的处理方法:
  • 分配他们
  • 将一个此种类型的变量赋值给另一个变量
  • 用FD_ZERO,FD_SET,FD_CLR和FD_ISSET宏指令来修改和检查他们。

基于I/O多路复用的并发事件驱动服务器

  • I/O多路复用可以用作事件并发驱动程序的基础。
  • 状态机:一组状态、输入事件、输出事件和转移。
  • 自循环:同一输入和输出状态之间的转移。

    I/O多路复用技术的优劣

  • 相比基于进程的设计给了程序员更多的对进程行为的控制,运行在单一进程上下文中,每个逻辑流都能访问全部的地址空间,在流之间共享数据很容易。
  • 编码复杂,随着并发粒度的减小,复杂性还会上升。粒度:每个逻辑流每个时间片执行的指令数量。

    12.3 基于线程的并发编程

  • 线程:运行在进程上下文中的逻辑流,由内核自动调度,有自己的线程上下文,包括一个唯一的整数线程ID,栈、栈指针、程序计数器、通用目的寄存器和条件码。所有运行在一个进程里的线程共享该进程的整个虚拟地址空间。

    线程执行模型

  • 每个进程开始生命周期时都是单一线程,这个线程称为主线程 (main thread)。在某一时刻,主线程创建一个对等线程 (peer thread),从这个时间点开始,两个线程就并发地运行。最后,因 为主线程执行一个慢速系统调用。或者因为它被系统的间隔计时器中断, 控制就会通过上下文切换传递到对等线程。对等线程会执行一段时间,然后控制传递回主线程,依次类推。

    Posix线程

  • Posix线程是C语言中处理线程的一个标准接口,允许程序创建、杀死和回收线程,与对等线程安全的共享数据。
  • 线程的代码和本地数据被封装在一个线程例程中。

    创建线程

  • 线程通过调用pthread_create来创建其他线程。
    int pthread_create(pthread_t *tid,pthread_attr_t *attr,func *f,void *arg); 成功则返回0,出错则为非零
  • 当函数返回时,参数tid包含新创建的线程的ID,新线程可以通过调用pthread_self函数来获得自己的线程ID。
    pthread_t pthread_self(void);返回调用者的线程ID。

    终止线程

  • 一个线程是通过以下方式之一来终止的。
  • 当顶层的线程例程返回时,线程会隐式地终止。
  • 通过调用pthread_exit函数,线程会显式地终止void pthread_exit(void *thread_return);

    回收已终止的线程资源

  • 线程通过调用pthread_join函数等待其他线程终止。
    int pthread_join(pthread_t tid,void **thread_return); 成功则返回0,出错则为非零

    分离线程

  • 在任何一个时间点上,线程是可结合或可分离的。一个可结合的线程能够被其他线程收回其资源和杀死,在被回收之前,它的存储器资源是没有被释放的。分离的线程则相反,资源在其终止时自动释放。
    int pthread_deacth(pthread_t tid); 成功则返回0,出错则为非零

    初始化线程

  • pthread_once允许初始化与线程例程相关的状态。
    pthread_once_t once_control=PTHREAD_ONCE_INIT; int pthread_once(pthread_once_t *once_control,void (*init_routine)(void)); 总是返回0
    第一步:服务器接受客户端的连接请求:

父进程为客户端 1 创建了子进程之后,它接受一个新的客户端 2 的连接请求, 并返回一个新的已连接描述符(比如描述符5),然后,父进程又派生另一个子进程,这个子进程用已连接描述符 5 为它的客户端提供服务。
此时,父进程正在等待下一个连接请求,而两个子进程正在并发地为它们各自的客户端提供服务。

  • 第二步:服务器派生一个子进程为这个客户端服务:

  • 第三步:服务器接受另一个连接请求:

12.4 多线程程序中的共享变量

  • 一个变量是共享的。当且仅当多个线程引用这个变量的某个实例。

    线程存储器模型

  • 每个线程都有自己独立的线程上下文,包括一个唯一的整数线程ID,栈、栈指针、程序计数器、通用目的寄存器和条件码。
  • 寄存器是从不共享的,而虚拟存储器总是共享的。
  • 各自独立的线程栈被保存在虚拟地址空间的栈区域中,并且通常是被相应的线程独立地访问的。

    将变量映射到存储器

  • 全局变量:定义在函数之外的变量
  • 本地自动变量:定义在函数内部但是没有static属性的变量。
  • 本地静态变量:定义在函数内部并有static属性的变量。

    共享变量

  • 当且仅当变量的一个实例被一个以上的线程引用时,就说变量是共享的。

    12.5用信号量同步线程

  • 共享变量的同时引入了同步错误,即没有办法预测操作系统是否为线程选择一个正确的顺序。

    进度图

  • 将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,将指令模型化为从一种状态到另一种状态的转换。

    信号量

  • P(s):如果s是非零的,那么P将s减一,并且立即返回。如果s为零,那么就挂起这个线程,直到s变为非零。
  • V(s):将s加一,如果有任何线程阻塞在P操作等待s变为非零,那么V操作会重启线程中的一个,然后该线程将s减一,完成他的P操作。
  • 信号量不变性:一个正确初始化了的信号量有一个负值。
  • 信号量操作函数:
  • int sem_init(sem_t *sem,0,unsigned int value);//将信号量初始化为value int sem_wait(sem_t *s);//P(s) int sem_post(sem_t *s);//V(s)

    使用信号量来实现互斥

  • 二元信号量(互斥锁):将每个共享变量与一个信号量s联系起来,然后用P(s)(加锁)和V(s)(解锁)操作将相应的临界区包围起来。
  • 禁止区:s<0,因为信号量的不变性,没有实际可行的轨迹线能够直接接触不安全区的部分

    12.6 使用线程来提高并行性

  • 并行程序的加速比通常定义为:

  • 其中,p为处理器核的数量,T为在p个核上的运行时间。

    12.7 其他并发问题

    可重入性

  • 可重入函数 (reentrant function),其特点在于它们具有这 样一种属性:当它们被多个线程调用时,不会引用任何共享数据。
  • 可重入函数通常要比不可重人的线程安全的函数高效一些,因为它们不需要同步操作。更进一步来说,将第 2 类线程不安全函数转化为线程安全函数的唯一方法就是重写它,使之变为可重入的。
  • 可重入函数、线程安全函数和线程不安全函数之间的集合关系:

  • 检查某个函数的代码并先验地断定它是可重入的。
  • 如果所有的函数参数都是传值传递的(即没有指针),并且所有的数据引用都是本地的自动栈变量(即没有引用静态或全局变量),那么函数就是显式可重入的 (explicitly reentrant),也就是说,无论它是被如何调用的,我们都可以断言它是可重入的。
  • 我们总是使用术语可重入的 (reenntrant) 既包括显式可重入函数也包括隐式可重入函数。然而,认识到可重入性有时既是调用者也是被调用者的属性,并不只是被调用者单独的属性是非常重要的。

    线程安全

  • 定义四个(不相交的)线程不安全函数类:
  • 不保护共享变量的函数。
  • 保持跨越多个调用状态的函数。
  • 返回指向静态变量指针的函数。
  • 调用线程不安全函数的函数。

    竞争

  • 当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达他的控制流x点时,就会发生竞争。
  • 为消除竞争,我么可以动态地为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针。

    死锁

  • 死锁:一组线程被阻塞了,等待一个永远也不会为真的条件。
  • 程序员使用P和V操作顺序不当,以至于两个信号量的禁止区域重叠。
  • 重叠的禁止区域引起了一组称为死锁区域的状态。
  • 死锁是一个相当难的问题,因为它是不可预测的。
  • 互斥锁加锁顺序规则:如果对于程序中每对互斥锁(s,t),给所有的锁分配一个全序,每个线程按照这个顺序来请求锁,并且按照逆序来释放,这个程序就是无死锁的。

小结:

  • 一个并发程序是由在时间上重叠的一组逻辑流组成的。
  • 三种不同的构建并发程序的机制:进程、I/O 多路复用和线程。
  • 进程是由内核自动调度的,而且因为它们有各自独立的虚拟地址空间,所以要实现共享数 据,必须要有显式的 IPC 机制。事件驱动程序创建它们自己的并发逻辑流,这些逻辑流被模型化为状态机,用I/O 多路复用来显式地调度这些流。因为程序运行在一个单一进程中,所以在流之间共享数据速度很快而且很容易。线程是这些方法的综合。同基于进程的流一样,线程也是由内核自动调度的。同基于 I/O 多路复用的流一样,线程是运行在一个单一进程的上下文中的,因 此可以快速而方便地共享数据。
  • 无论哪种并发机制,同步对共享数据的并发访问都是一个困难的问题。提出对信号量的 P 和 V操作就是为了帮助解决这个问题。信号量操作可以用来提供对共享数据的互斥访问,也对诸如生产者一消费者程序中有限缓冲区和读者一写者系统中的共享对象这样的资源访问进行调度。
  • 并发也引人了其他一些困难的问题。被线程调用的函数必须具有一种称为线程安全的属性。
  • 可重入函数是线程安全函数的一个真子集,它不访问任何共享数据。可重入函数通常比不可重人函数更为有效,因为它们不需要任何同步原语。竞争和死锁是并发程序中出现的另一些困难的问题。当程序员错误地假设逻辑流该如何调度时,就会发生竞争。当一个流等待一个永远不会发生的事件时,就会产生死锁。

教材学习中的问题和解决过程:

:练习题12.1:第33行代码,父进程关闭了连接描述符后,子进程仍然可以使用该描述符和客户端通信。为什么?

  • 答:当父进程派生子进程是,它得到一个已连接描述符的副本,并将相关文件表中的引用计数从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:在下图1中基于进程的服务器中,我们在两个位置小心地关闭了已连接描述符:父进程和子进程。然而,在图2中,基于线程的服务器中,我们只在一个位置关闭了已连接描述符:对等线程,为什么?

  • 答:因为线程运行在同一个进程中,他们共享同一个描述符表,所以在描述符表中的引用计数与线程的多少是没有关系的都为1,因此只需要一个close就够了。

    练习题12.9:设p表示生产者数量,c表示消费者数量,n表示以项目单元为单位的缓冲区大小。对于下面的美国场景,指出subfinsert和subfremove中的互斥信号量是否是必需的。

A.p =1,c =1,n>1

B.p =1,c=1,n=1

C.p>1,c>1,n=1
  • A:是。因为生产者和消费者会并发地访问缓冲区
  • B:不是。因为n=1,一个非空的缓冲区就相当于一个满的缓冲区。当缓冲区包含一个项目的时候,生产者就已经被阻塞了;当缓冲区为空的时候,消费者就被阻塞了。所以在任意时刻,只有一个线程可以访问缓冲区,不必加互斥锁。
  • C:不是。同上。

练习题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\n");
  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!\n");
  return NULL;
}

代码调试中的问题和解决过程

condvar.c

#include <stdlib.h>
#include <pthread.h>
#include <stdlib.h>

typedef struct _msg{
    struct _msg * next;
    int num;
} msg;

msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer ( void * p )
{
    msg * mp;

    for( ;; ) {
        pthread_mutex_lock( &lock );
        while ( head == NULL )
            pthread_cond_wait( &has_product, &lock );
        mp = head;
        head = mp->next;
        pthread_mutex_unlock ( &lock );
        printf( "Consume %d tid: %d\n", mp->num, pthread_self());
        free( mp );
        sleep( rand() % 5 );
    }
}

void *producer ( void * p )
{
    msg * mp;
    for ( ;; ) {
        mp = malloc( sizeof(msg) );
        pthread_mutex_lock( &lock );
        mp->next = head;
        mp->num = rand() % 1000;
        head = mp;
        printf( "Produce %d tid: %d\n", mp->num, pthread_self());
        pthread_mutex_unlock( &lock );
        pthread_cond_signal( &has_product );
        sleep ( rand() % 5);
    }
}

int main(int argc, char *argv[] )
{
    pthread_t pid1, cid1;
    pthread_t pid2, cid2;
    srand(time(NULL));
    pthread_create( &pid1, NULL, producer, NULL);
    pthread_create( &pid2, NULL, producer, NULL);
    pthread_create( &cid1, NULL, consumer, NULL);
    pthread_create( &cid2, NULL, consumer, NULL);
    pthread_join( pid1, NULL );
    pthread_join( pid2, NULL );
    pthread_join( cid1, NULL );
    pthread_join( cid2, NULL );
    return 0;
}

运行结果

  • mutex用于保护资源,wait函数用于等待信号,signal函数用于通知信号,wait函数中有一次对mutex的释放和重新获取操作,因此生产者和消费者并不会出现死锁。

    createthread.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

pthread_t ntid;

void printids( const char *s )
{
    pid_t pid;
    pthread_t tid;

    pid = getpid();
    tid = pthread_self();
    printf("%s pid %u tid %u (0x%x) \n", s , ( unsigned int ) pid,
                ( unsigned int ) tid, (unsigned int ) tid);
}

void *thr_fn( void * arg )
{
    printids( arg );
    return NULL;
}

int main( void )
{
    int err;

    err = pthread_create( &ntid, NULL, thr_fn, "new thread: " );
    if ( err != 0 ){
        fprintf( stderr, "can't create thread: %s\n", strerror( err ) );
        exit( 1 );
    }
    printids( "main threads: " );
    sleep(1);
    return 0;
}

运行结果

  • 打印进程和线程ID

    share.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
char buf[BUFSIZ];

void *thr_fn1( void *arg )
{
    printf("thread 1 returning %d\n", getpid());
    printf("pwd:%s\n", getcwd(buf, BUFSIZ));
    *(int *)arg = 11;
    return (void *) 1;
}

void *thr_fn2( void *arg )
{
    printf("thread 2 returning %d\n", getpid());
    printf("pwd:%s\n", getcwd(buf, BUFSIZ));
    pthread_exit( (void *) 2 );
}

void *thr_fn3( void *arg )
{
    while( 1 ){
        printf("thread 3 writing %d\n", getpid());
        printf("pwd:%s\n", getcwd(buf, BUFSIZ));
        sleep( 1 );
    }
}
int n = 0;

int main( void )
{
    pthread_t tid;
    void *tret;

    pthread_create( &tid, NULL, thr_fn1, &n);
    pthread_join( tid, &tret );
    printf("n= %d\n",  n );
    printf("thread 1 exit code %d\n", (int) tret );

    pthread_create( &tid, NULL, thr_fn2, NULL);
    pthread_join( tid, &tret );
    printf("thread 2 exit code %d\n", (int) tret );
    
    pthread_create( &tid, NULL, thr_fn3, NULL);
    sleep( 3 );
    pthread_cancel(tid);
    pthread_join( tid, &tret );
    printf("thread 3 exit code %d\n", (int) tret );
    
}

运行结果

  • 获得线程的终止状态,thr_fn 1,thr_fn 2和thr_fn 3三个函数对应终止线程的三种方法,即从线程函数return,调用pthread_exit终止自己和调用pthread_cancel终止同一进程中的另一个线程。

    countwithmutex.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define NLOOP 5000

int counter;

pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void *doit( void * );

int main(int argc, char **argv)
{
    pthread_t tidA, tidB;

    pthread_create( &tidA ,NULL, &doit, NULL );
    pthread_create( &tidB ,NULL, &doit, NULL );

    pthread_join( tidA, NULL );
    pthread_join( tidB, NULL );

    return 0;
}

void * doit( void * vptr)
{
    int i, val;

    for ( i=0; i<NLOOP; i++ ) {
        pthread_mutex_lock( &counter_mutex );
        val = counter++;
        printf("%x: %d \n", (unsigned int) pthread_self(), val + 1);
        counter = val + 1;
        pthread_mutex_unlock( &counter_mutex );
    }
    return NULL;
}

运行结果

  • 引入互斥锁(Mutex),获得锁的线程可以完成”读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据。

    semphore.c

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <semaphore.h>

#define NUM 5
int queue[NUM];
sem_t blank_number, product_number;

void *producer ( void * arg )
{
    static int p = 0;

    for ( ;; ) {
        sem_wait( &blank_number );
        queue[p] = rand() % 1000;
        printf("Product %d \n", queue[p]);
        p = (p+1) % NUM;
        sleep ( rand() % 5);
        sem_post( &product_number );
    }
}
void *consumer ( void * arg )
{

    static int c = 0;
    for( ;; ) {
        sem_wait( &product_number );
        printf("Consume %d\n", queue[c]);
        c = (c+1) % NUM;
        sleep( rand() % 5 );
        sem_post( &blank_number );
    }
}

int main(int argc, char *argv[] )
{
    pthread_t pid, cid;
    
    sem_init( &blank_number, 0, NUM );
    sem_init( &product_number, 0, 0);
    pthread_create( &pid, NULL, producer, NULL);
    pthread_create( &cid, NULL, consumer, NULL);
    pthread_join( pid, NULL );
    pthread_join( cid, NULL );
    sem_destroy( &blank_number );
    sem_destroy( &product_number );
    return 0;
}

运行结果:

  • semaphore表示信号量,semaphore变量的类型为sem_t,sem_init()初始化一个semaphore变量,value参数表示可用资源 的数量,pshared参数为0表示信号量用于同一进程的线程间同步。

    count.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter;
void *doit( void * );
int main(int argc, char **argv)
{
    pthread_t tidA, tidB;

    pthread_create( &tidA ,NULL, &doit, NULL );
    pthread_create( &tidB ,NULL, &doit, NULL );

    pthread_join( tidA, NULL );
    pthread_join( tidB, NULL );

    return 0;
}
void * doit( void * vptr)
{
    int i, val;

    for ( i=0; i<NLOOP; i++ ) {
        val = counter++;
        printf("%x: %d \n", (unsigned int) pthread_self(), val + 1);
        counter = val + 1;
    }

}

运行结果:

  • 这是一个不加锁的创建两个线程共享同一变量都实现加一操作的程序,在这个程序中虽然每个线程都给count加了5000,但由于结果的互相覆盖,最终输出值不是10000,而是5000。

猜你喜欢

转载自www.cnblogs.com/wjs123456/p/12079584.html