zoukankan      html  css  js  c++  java
  • 20155236 《信息安全系统设计基础》第8周学习总结

    20155236 《信息安全系统设计基础》第8周学习总结

    网络编程

    • 套接字接口概述:

    并发编程

    • 并发:逻辑控制流在时间上重叠
    • 并发程序:使用应用级并发的应用程序称为并发程序。
    • 三种基本的构造并发程序的方法:
      • 进程,用内核来调用和维护,有独立的虚拟地址空间,显式的进程间通信机制。
      • I/O多路复用,应用程序在一个进程的上下文中显式的调度控制流。逻辑流被模型化为状态机。
      • 线程,运行在一个单一进程上下文中的逻辑流。由内核进行调度,共享同一个虚拟地址空间。

    基于进程的并发编程

    • 构造并发程序最简单的方法——用进程。常用函数如下:fork,exec,waitpid
    • 构造并发服务器:在父进程中接受客户端连接请求,然后创建一个新的子进程来为每个新客户端提供服务。
    • 需要注意的事情:
      • 父进程需要关闭它的已连接描述符的拷贝(子进程也需要关闭)
      • 必须要包括一个SIGCHLD处理程序来回收僵死子进程的资源
      • 父子进程之间共享文件表,但是不共享用户地址空间
    • 独立地址空间的优点是防止虚拟存储器被错误覆盖,缺点是开销高,共享状态信息才需要IPC机制

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

    • echo服务器必须响应两个相互独立的I/O时间:
      • 网络客户端发起连接请求
      • 用户在键盘上键入命令行
    • I/O多路复用技术的基本思路:使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
    • 将描述符集合看成是n位位向量:b(n-1),……b1,b0 ,每个位bk对应于描述符k,当且仅当bk=1,描述符k才表明是描述符集合的一个元素。可以做以下三件事:
      • 分配;
      • 将一个此种类型的变量赋值给另一个变量;
      • 用FDZERO、FDSET、FDCLR和FDISSET宏指令来修改和检查它们。
    • echo函数:将来自科幻段的每一行回送回去,直到客户端关闭这个链接。
    • 状态机就是一组状态、输入事件和转移,转移就是将状态和输入时间映射到状态,自循环是同一输入和输出状态之间的转移。
    • 事件驱动器的设计优点:
      • 比基于进程的设计给了程序员更多的对程序行为的控制
      • 运行在单一进程上下文中,因此,每个逻辑流都能访问该进程的全部地址空间,使得流之间共享数据变得很容易。
      • 不需要进程上下文切换来调度新的流。
    • 缺点:
      • 编码复杂
      • 不能充分利用多核处理器
    • 粒度:每个逻辑流每个时间片执行的指令数量。并发粒度就是读一个完整的文本行所需要的指令数量。

    基于线程的并发编程

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

    线程执行模型

    • 主线程:每个进程开始生命周期时都是单一线程。
      对等线程:某一时刻,主线程创建的对等线程
    • 线程与进程的不同:
      • 线程的上下文切换要比进程的上下文切换快得多;
      • 和一个进程相关的线程组成一个对等池,独立于其他线程创建的线程。
      • 主线程和其他线程的区别仅在于它总是进程中第一个运行的线程。
    • 对等池的影响
      • 一个线程可以杀死它的任何对等线程;
      • 等待它的任意对等线程终止;
      • 每个对等线程都能读写相同的共享资源。

    Posix线程

    • 线程例程:线程的代码和本地数据被封装在一个线程例程中。每一个线程例程都以一个通用指针作为输入,并返回一个通用指针。

    创建线程

    • pthread create函数创建一个新的线程,并带着一个输入变量arg,在新线程的上下文中运行线程例程f。新线程可以通过调用pthread _self函数来获得自己的线程ID。

    终止线程

    • 一个线程的终止方式:
      • 当顶层的线程例程返回时,线程会隐式的终止;
      • 通过调用pthread _exit函数,线程会显示地终止。如果主线程调用pthread _exit,它会等待所有其他对等线程终止,然后再终止主线程和整个进程。

    回收已终止线程的资源

    • pthread _join函数会阻塞,直到线程tid终止,回收已终止线程占用的所有存储器资源。pthread _join函数只能等待一个指定的线程终止。

    分离线程

    • 在任何一个时间点上,线程是可结合的或者是分离的。一个可结合的线程能够被其他线程收回其资源和杀死;一个可分离的线程是不能被其他线程回收或杀死的。它的存储器资源在它终止时有系统自动释放。
    • 默认情况下,线程被创建成可结合的,为了避免存储器漏洞,每个可集合的线程都应该要么被其他进程显式的回收,要么通过调用pthread _detach函数被分离。

    初始化线程

    • pthread _once函数允许初始化与线程例程相关的状态。
    • once _control变量是一个全局或者静态变量,总是被初始化为PTHREAD _ONCE _INIT

    一个基于线程的并发服务器

    • 对等线程的赋值语句和主线程的accept语句之间引入了竞争。

    多线程程序中的变量共享

    线程存储器模型

    • 每个线程和其他线程一起共享进程上下文的剩余部分。包括整个用户虚拟地址空间,是由只读文本、读/写数据、堆以及所有的共享库代码和数据区域组成的。线程也共享同样的打开文件的集合。
    • 任何线程都可以访问共享虚拟存储器的任意位置。寄存器是从不共享的,而虚拟存储器总是共享的。

    将变量映射到存储器

    • 全局变量:虚拟存储器的读/写区域只会包含每个全局变量的一个实例。
    • 本地自动变量:定义在函数内部但没有static属性的变量。
    • 本地静态变量:定义在函数内部并有static属性的变量。

    共享变量

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

    用信号量同步线程

    • 共享变量引入了同步错误的可能性。
    • 线程i的循环代码分解为五部分:
      • Hi:在循环头部的指令块
      • Li:加载共享变量cnt到寄存器%eax的指令,%eax表示线程i中的寄存器%eax的值
      • Ui:更新(增加)%eax的指令
      • Si:将%eaxi的更新值存回到共享变量cnt的指令
      • Ti:循环尾部的指令块。

    进度图

    • 进度图将指令执行模式化为从一种状态到另一种状态的转换。转换被表示为一条从一点到相邻点的有向边。合法的转换是向右或者向上。
    • 临界区:对于线程i,操作共享变量cnt内容的指令构成了一个临界区。
    • 互斥的访问:确保每个线程在执行它的临界区中的指令时,拥有对共享变量的互斥的访问。
    • 安全轨迹线:绕开不安全区的轨迹线
    • 不安全轨迹线:接触到任何不安全区的轨迹线就叫做不安全轨迹线
    • 任何安全轨迹线都能正确的更新共享计数器。

    信号量

    • 当有多个线程在等待同一个信号量时,你不能预测V操作要重启哪一个线程。
    • 信号量不变性:一个正在运行的程序绝不能进入这样一种状态,也就是一个正确初始化了的信号量有一个负值。
    • 信号量定义:
    type semaphore=record
     count: integer;
     queue: list of process
    end;
     var s:semaphore;
    

    使用信号量来实现互斥

    • 基本思想是将每个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P和V操作将相应的临界区包围起来。
    • 几个概念
      • 二元信号量:用这种方式来保护共享变量的信号量叫做二元信号量,取值总是0或者1.
      • 互斥锁:以提供互斥为目的的二元信号量
      • 加锁:对一个互斥锁执行P操作
      • 解锁;对一个互斥锁执行V操作
      • 计数信号量:被用作一组可用资源的计数器的信号量
      • 禁止区:由于信号量的不变性,没有实际可能的轨迹能够包含禁止区中的状态。

    使用线程提高并行性

    • 写顺序程序只有一条逻辑流,写并发程序有多条并发流,并行程序是一个运行在多个处理器上的并发程序。并行程序的集合是并发程序集合的真子集。

    其他并发问题

    线程安全

    • 线程安全:当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。
    • 线程不安全:如果一个函数不是线程安全的,就是线程不安全的。
    • 线程不安全的类:
      • 不保护共享变量的函数
      • 保持跨越多个调用的状态的函数。
      • 返回指向静态变量的指针的函数。解决办法:重写函数和加锁拷贝。
      • 调用线程不安全函数的函数。

    可重入性

    • 可重入函数:当它们被多个线程调用时,不会引用任何共享数据。可重入函数是线程安全函数的一个真子集 。
    • 关键思想是我们用一个调用者传递进来的指针取代了静态的next变量。
    • 显式可重入:没有指针,没有引用静态或全局变量
      隐式可重入:允许它们传递指针
    • 可重入性即使调用者也是被调用者的属性,并不只是被调用者单独的属性。

    在线程化的程序中使用已存在的库函数

    • 使用线程不安全函数的可重入版本,名字以_r为后缀结尾。

    竞争

    • 竞争发生的原因:
      • 一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点。也就是说,程序员假定线程会按照某种特殊的轨迹穿过执行状态空间,忘了一条准则规定:线程化的程序必须对任何可行的轨迹线都正确工作。
    • 消除方法:动态的为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针

    死锁

    • 死锁:一组线程被阻塞了,等待一个永远也不会为真的条件。

    • 程序员使用P和V操作顺序不当,以至于两个信号量的禁止区域重叠。

    • 重叠的禁止区域引起了一组称为死锁区域的状态。

    • 死锁是一个相当难的问题,因为它是不可预测的。

    • 互斥锁加锁顺序规则:如果对于程序中每对互斥锁(s,t),给所有的锁分配一个全序,每个线程按照这个顺序来请求锁,并且按照逆序来释放,这个程序就是无死锁的。

    • 程序首先定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁。先创建tidA线程后运行doit函数,利用互斥锁锁定资源,进行计数,执行完毕后解锁。后创建tidB,与tidA交替执行。由于定义的NLOOP值为5000,所以程序最后的输出值为10000.程序的最后还需要分别回收tidA和tidB的资源。

    • 相对于前一个实例,这个代码中加了“互斥锁”(Mutex),在其中一个线程(获得锁)执行时,另一个(未获得)只能等待,所以产生了不同于count.c的输出效果。

    本周结对学习情况

    • 12.4.1 线程存储器模型
      1、每个线程和其他线程一起共享进程上下文的剩余部分。包括整个用户虚拟地址空间,是由只读文本、读/写数据、堆以及所有的共享库代码和数据区域组成的。线程也共享同样的打开文件的集合。
      2、任何线程都可以访问共享虚拟存储器的任意位置。寄存器是从不共享的,而虚拟存储器总是共享的。
    • 12.4.2 将变量映射到存储器
      1、全局变量:虚拟存储器的读/写区域只会包含每个全局变量的一个实例。
      2、本地自动变量:定义在函数内部但没有static属性的变量。
      3、本地静态变量:定义在函数内部并有static属性的变量。
    • 12.4.3 共享变量
      变量v是共享的,当且仅当它的一个实例被一个以上的线程引用。

    其他(感悟、思考等,可选)

    • 并发是一个之前没有见过的不同的机制,说没见过也不可能,我们使用的任何一个操作系统,哪个是只能在一个时间段上运行一个程序吗,都是可以重叠的,而经过本章节的学习,从程序级的角度了解到了并发,并进行了实践,这就是对书本理论的一个巩固。
    • 但感觉自己还是没有太弄懂。

    课下作业 -2

    把课上练习3的daytime服务器分别用多进程和多线程实现成并发服务器并测试

    在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多任务。现代的操作系统几乎都是多任务操作系统,能够同时管理多个进程的运行。多任务带来的好处是明显的,比如你可以边听mp3边上网,与此同时甚至可以将下载的文档打印出来,而这些任务之间丝毫不会相互干扰。那么这里就涉及到并行的问题,俗话说,一心不能二用,这对计算机也一样,原则上一个CPU只能分配给一个进程,以便运行这个进程。我们通常使用的计算机中只有一个CPU,也就是说只有一颗心,要让它一心多用,同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”,它的思想简单介绍如下:在操作系统的管理下,所有正在运行的进程轮流使用CPU,每个进程允许占用CPU的时间非常短(比如10毫秒),这样用户根本感觉不出来CPU是在轮流为多个进程服务,就好象所有的进程都在不间断地运行一样。但实际上在任何一个时间内有且仅有一个进程占有CPU。如果一台计算机有多个CPU,情况就不同了,如果进程数小于CPU数,则不同的进程可以分配给不同的CPU来运行,这样,多个进程就是真正同时运行的,这便是并行。但如果进程数大于CPU数,则仍然需要使用并发技术。在Windows中,进行CPU分配是以线程为单位的,一个进程可能由多个线程组成,这时情况更加复杂,但简单地说,有如下关系:

    • 总线程数<= CPU数量:并行运行
    • 总线程数 > CPU数量:并发运行
    • 并行运行的效率显然高于并发运行,所以在多CPU的计算机中,多任务的效率比较高。但是,如果在多CPU计算机中只运行一个进程(线程),就不能发挥多CPU的优势。这里涉及到多任务操作系统的问题,多任务操作系统(如Windows)基本原理是:操作系统将CPU的时间片分配给多个线程,每个线程在操作系统指定的时间片内完成(注意,这里的多个线程是分属于不同进程的).操作系统不断的从一个线程的执行切换到另一个线程的执行,如此往复,宏观上看来,就好像是多个线程在一起执行.由于这多个线程分属于不同的进程,因此在我们看来,就好像是多个进程在同时执行,这样就实现了多任务。

    从书上找到相关代码:
    1.

    #include "csapp.h"
    void echo(int connfd);
    int main(int argc, char **argv) 
    {
        int listenfd, connfd, port, clientlen;
        struct sockaddr_in clientaddr;
        struct hostent *hp;
        char *haddrp;
        if (argc != 2) {
        fprintf(stderr, "usage: %s <port>
    ", argv[0]);
        exit(0);
        }
        port = atoi(argv[1]);
    
        listenfd = Open_listenfd(port);
        while (1) {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
    
        /* determine the domain name and IP address of the client */
        hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr, 
                   sizeof(clientaddr.sin_addr.s_addr), AF_INET);
        haddrp = inet_ntoa(clientaddr.sin_addr);
        printf("server connected to %s (%s)
    ", hp->h_name, haddrp);
    
        echo(connfd);
        Close(connfd);
        }
        exit(0);
    }              // sever
    
    #include "csapp.h"
    int main(int argc, char **argv) 
    {
        int clientfd, port;
        char *host, buf[MAXLINE];
        rio_t rio;
    
        if (argc != 3) {
        fprintf(stderr, "usage: %s <host> <port>
    ", argv[0]);
        exit(0);
        }
        host = argv[1];
        port = atoi(argv[2]);
    
        clientfd = Open_clientfd(host, port);
        Rio_readinitb(&rio, clientfd);
    
        while (Fgets(buf, MAXLINE, stdin) != NULL) {
        Rio_writen(clientfd, buf, strlen(buf));
        Rio_readlineb(&rio, buf, MAXLINE);
        Fputs(buf, stdout);
        }
        Close(clientfd);
        exit(0);
    }  // client
    /* $end echoclientmain */
    

    并做一些修改。

    我们分别创建两个文件夹,多进程和多线程,先编译。
    gcc -c echoclient.c
    gcc -o echoclient csapp.o echoclient.o -lpthread
    别忘了要输入自己的IP,和端口号,如下图:

    同理,在另一个文件夹中,做如下工作。

    参考资料

  • 相关阅读:
    天猫弹性导航栏
    php 中构造函数和析构函数
    web服务器集群(多台web服务器)后session如何同步和共享
    mycat中schema.xml的一些解释
    mycat高可用集群搭建
    mycat水平分表
    mycat实现mysql数据库的垂直切分
    高并发、大流量解决方案
    nginx负载均衡六种策略
    mysql主从复制实现
  • 原文地址:https://www.cnblogs.com/fcgfcgfcg/p/7819865.html
Copyright © 2011-2022 走看看