20145223《信息安全系统设计》第13周学习总结

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

第十一章 网络编程

客户端-服务端编程模型

·每个网络应用都是基于客户端-服务器模型的。

·一个应用是由一个服务器进程和一个或多个客户端进程组成。

·客户端-服务器模型中的基本操作是事务。

·一个客户端-服务器事物由四步组成:(客户端和服务器端是相对于进程而言的

网络

·对于一个主机,网络是一种I/O设备,作为数据源和数据接收方,典型地是通过DMA(直接存储器存取方式)传送

·物理上而言,网络是一个按照地理远近组成的层次系统。最底层是LAN(局域网)

·一个以太网段,包括电缆和集线器,每根电缆都有相同的最大位带宽,集线器不加分辩地将一个端口上收到的每个位复制到其他所有的端口上。因此,每台主机都能看到每个位。

·每个以太网适配器都有一个全球唯一的48位地址,存储在适配器的非易失性存储器上。

·一台主机可以发送一段位:,到这个网段内其它任何主机。

·每个帧包括一些固定数量的头部位header(标识此帧的源和目的地址及帧长)和紧随其后的数据位(有效载荷)。

·每个主机都能看到这个帧,但是只有目的主机能读取。

·使用电缆和网桥,多个以太网段可以连接成较大的局域网,称为桥接以太网。这些电缆的带宽可以是不同的。

·在层次的更高级别中,多个不兼容的局域网可以通过叫做路由器的特殊计算机连接起来,组成一个互联网络(Internet

互联网络如何实现跨过不兼容发送数据?

·解决办法:一层运行在每台主机和路由器之间的协议软件,它消除了不同网络之间的差异

·这种协议必须提供的两种基本能力:

(1)命名机制

(2)传送机制

结合书本618页的八个步骤和图可以理解在互联网上,数据是如何在主机之间相互传递的:

全球IP因特网

·每台因特网主机都运行TCP/IP协议的软件。

·因特网的客户端和服务器混合使用套接字接口函数和Unix I/O函数进行通信

·TCP/IP实际上是一个协议族

·把因特网看做一个世界范围的主机集合,满足以下特性:
(1)主机集合被映射为一组32位的IP地址
(2)这组IP地址被映射为一组称为因特网域名的标识符
(3)因特网主机上的进程能够通过连接和任何其他主机上的进程通信

IP地址

·一个IP地址就是一个32位无符号整数。

·IP地址通常以点分十进制表示法来表示。

·在linux系统上可以用hostname命令确定自己主机的点分十进制地址

因特网域名

·因特网应用程序通过调用gethostbyname函数和gethostbyaddr函数,从DNS数据库中检索任意的主机条目。

gethostbyname函数:返回和域名name相关的主机条目。

gethostbyaddr函数:返回和IP地址相关联的主机条目。
因特网链接

·一个套接字是连接的一个端点。

·每个套接字都有相应的套接字地址,由一个因特网地址和一个16位的整数端口组成的,用“地址:端口”来表示。

·一个连接是由它两端的套接字地址惟一确定的。这对套接字地址叫做套接字对。

套接字接口

·套接字接口是一组用来结合unit I/O函数创建网络应用的函数。

套接字地址结构

从unit内核的角度来看,套接字就是通信的端点;从unix程序的角度来看,套接字就是一个有相应描述符的打开文件。

函数

(1)sockte函数:创建一个套接字描述符。
(2)connect函数:建立和服务器的连接。
(3)open_clientfd函数:将socket和connect函数包装而成。客户端可以用它来和服务器建立连接。
(4)bind函数、listen函数、accept函数:均被服务器用于和客户端建立连接。
(5)open_listenfd函数:socket、bind和listen函数结合。用于服务器创建一个监听描述符。
(6)accept函数

5.Web服务器

Web基础

·客户端和服务器之间一个交互用的是基于文本的应用级协议——HTTP

·Web服务和常规文件检索服务区别:Web内容可以用一个叫做HTML的语言来编写。

Web内容

·Web服务端以两种不同方式向客户端提供内容:
(1)服务静态内容
(2)服务动态内容

HTTP事务

1.HTTP请求(方法:GET POST OPTIONS HEAD PUT DELETE TRACE)

2.HTTP响应

服务动态内容

1.客户端如何将程序参数传递给服务器

2.服务器如何将参数传递给子进程

3.服务器如何将其他信息传递给子进程

4.子进程将它的输出发送到哪儿

第十二章 并发编程

操作系统提供了三种基本的构造并发程序的方法:

(1)进程。每个逻辑控制流都是一个进程,由内核来调度和维护。
(2)I/O多路复用。
(3)线程。

基于进程的并发服务器

一个基于进程的并发的echo服务器的代码,重要说明:

·首先,通常服务器会运行很长时间,所以我们必须包括一个SIGCHLD处理程序,来回收僵死子进程资源。

·其次,父子进程必须关闭他们的connfd拷贝。

·最后,因为套接字的文件表表项的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。

关于进程的优劣

·关于进程的优劣,对于在父、子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。进程有独立的地址空间既是优点又是缺点。这样一来,一个进程不可能不小心覆盖另一个进程的虚拟存储器。另一方面,独立的地址空间使得进程共享状态信息变得更加困难

基于i/o多路复用的并发编程

·假设要求你编写一个echo服务器,它也能对用户从标准输入键入的交互命令做出响应。在这种情况下,服务器必须响应两个相互独立的I/O事件:1)网络客户端发起连接请求,2)用户在键盘上键入命令行。如果accept中等待每一个连接请求,那么无法响应输入命令。如果在read中等待一个输入命令,我们就不能响应任何连接请求。

针对这种困境的一个解决办法就是I/O多路复用技术。基本的思路就是使用select函数,要求内核挂起进程,只有在一个或者多个I/O事件发生后,才将控制返给应用程序。

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

·I/O多路复用可以用做并发事件驱动程序的基础,在事件驱动程序中,流是因为某种事件而前进的。一般概念是将逻辑流模型化为状态机,不严格地说,一个状态机就是一组状态,输入事件和转移,其中转移就是将状态和输入事件映射到状态,每个转移都将一个(输入状态,输入事件)对映射到一个输出状态,自循环是同一输入和输出状态之间的转移,通常把状态机画成有向图,其中节点表示状态,有向弧表示转移,而弧上的标号表示输人事件,一个状态机从某种初始状态开始执行,每个输入事件都会引发一个从当前状态到下一状态的转移。

i/o多路复用技术的优劣

·事件驱动设计的一个优点是,它比基于进程的设计给了程序员更多的对程序行为的控制。另一个优点是,一个基于I/O多路复用的事件驱动器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间。最后,事件驱动设计常常比基于进利的设计要高效得多,因为它们不需要进程上下文切换来调度新的流。

·事件驱动设计的一个明显的缺点就是编码复杂,我们的事件驱动的并发echo服务器需要的代码比基于进程的代码多三倍。基于事件的设计的另一个重大缺点是它们不能充分利利用多核处理器。

基于线程的并发编程

·线程就是运行在进程上下文中的逻辑流。每个线程都有自己的线程上下文,包括一个线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码。多个线程运行在单一进程的上下文中,因此共享这个进程虚拟地址空间的整个内容,包括它的代码、数据、堆、共享库和打开的文件。

线程执行模型

·多线程的执行模型在某些方面和多进程的执行模型是相似的。每个进程开始生命周期时都是单一线程,这个进程称为主线程。在某一时刻,主线程创建一个对等线程,从这个时间点开始,两个线程就并发地运行。

Posix线程

·Posix线程是在C程序中处理线程的一个标准接口。Pthreads定义了大约60个函数,允许程序创建、杀死和回收线程,与对等线程安全地共享数据,还可以通知对等线程系统状态的变化。

创建线程

·线程通过调用pthread_create函数来创建其他进程。

·当pthread_create返回时,参数tid包含新创建进程的ID新进程可以通过调用pthread_self获得自己的线程ID:

终止线程

·一个线程是以下列方式之一来终止的:
(1)当顶层的线程例程返回时,线程会隐式地终止。
(2)通过调用pthread_exit函数,线程会显它会等待所有其他对等线程终止,然后再终止式地终止。
(3)某个对等线程调用Unix的exit函数,该函数终止进程以及所有与该进程相关的线程。
(4)另一个对等线程通过以当前线程ID作为参数调用pthread_cancle函数来终止进程。

回收已终止线程的资源

·线程通过调用pthread_join函数等待其他线程终止。

分离线程

初始化线程

多线程程序中的共享变量

·为了理解C程序中的一个变量是否是共享的,有一些基本的问题要解答:1)线程的基础存储器模型是什么?2)根据这个模型,变量实例是如何映射到存储器的?3)最后,有多少线程引用这些实例?一个变量是共享的,当且仅当多个线程引用这个变量的某个实例。

线程存储器模型

·一组并发线程运行在一个进程的上下文中。每个线程都有它自己独立的线程上下文,包括线程ID、栈、栈指针、程序计数器、条件码和通用目的寄存器值。每个线程和其他线程一起共享进程上下文的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本代码、读/写数据、堆以及所有的共享库代码和数据区域组成的。线程也共享同样的打开文件的集合。

·从实际操作的角度来说,让一个线程去读或写另一个线程的寄存器值是不可能的。另一方面,任何线程都可以访问共享虚拟存储器的任意位置。

·各自独立的线程栈的存储器模型不是那么整齐清楚的。这些栈被保存在虚拟地址空间的栈区域中,并且通常是被相应的线程独立地访问的。

将变量映射到存储器

·线程化的C程序中变量根据它们的存储类型被映射到虚拟存储器:
(1)全局变量。全局变量是定义在函数之外的变量。
(2)本地自动变量。本地自动变量就是定义在函数内部但是没有static属性的变量。
(3)本地静态变量。本地静态变量是定义在函数内部并有static属性的变量。

共享变量

·我们说一个变量V是共享的,当且仅当它的一个实例被一个以上的线程引用。

使用信号量来实现互斥

进度图——将n个并发进程的执行模型化为一条n维笛卡尔空间中的轨迹线。

信号量:

·信号量s是具有非负整数值的全局变量,只能由两种特殊的操作来处理,这两种操作称为P和V。

P(s):如果s是非零的,那么P将s减1并且立即返回。如果s为零,那么就挂起这个线程,直到s变为非零,而一个v操作会重启这个线程。

V(s):V操作将s加1。如果有任何线程阻塞在P操作等待s变成非零,那么V操作会重启这些线程中的一个,然后该线程将s减1,完成它的P操作。
·当有多个线程在等待同一个信号量时,你不能预测V操作要重启哪一个进程。

使用信号量来实现互斥

·信号量提供了一种很方便的方法来确保对共享变量的互斥访问。基本思想是将每个共享变量与一个信号量s(初始为1)联系起来 ,然后用P(s)和V(s)操作将相应的临界区包围起来。以这种方式来保护共享变量的信号量叫做二元信号量,因为它的值总是0或者1。以提供互斥为目的的二元信号量常常也称为互斥锁。在一个互斥锁上执行P操作称为对互斥锁加锁。类似地,执行V操作称为对互斥锁解锁。对一个互斥锁加了锁但是还没有解锁的线程称为占用这个互斥锁。一个被用作一组·可用资源的计数器的信号量称为计数信号量。

·关键思想是这种P和V操作的结合创建了一组状态,叫做禁止区。

利用信号量来调度共享资源:

1.生产者-消费者问题

2.读者-写着问题

其他并发问题

线程安全

·一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的。

·定义出四个线程不安全函数类:

(1)不保护共享变量的函数。利用P和V操作这样的同步操作来保护共享的变量。
(2)保持跨越多个调用的状态函数。
(3)返回指向静态变量的指针的函数。
(4)调用线程不安全函数的函数。

可重入性

·有一类重要的线程安全函数,叫做可重入函数,其特点在于他们具有这样一种属性:当它们被多个线程调用时,不会引用任何共享数据。

·重入函数通常要比不可重入的线程安全的函数高效一些,因为它们不需要同步操作。更进一步来说,将第2类线程不安全函数转化为线程安全函数的唯一方法就是重写它,使之变为可重入的。

竞争

·当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的X点时,就会发生竞争。通常发生竞争是因为程序员假定线程将按照某种特殊的轨迹线穿过执行状态空间,而忘记了另一条准则规定:线程化的程序必须对任何可行的轨迹线都正确工作。

死锁

·信号量引入了一种潜在的令人厌恶的运行时错误,叫做死锁。它指的是一组线程被阻塞了,等待一个永远也不会为真的条件。进度图对于理解死锁是一个无价的工具。

·关于死锁的重要知识:
(1)程序员使用P和V操作漏序不当,以至于两个信号量的禁止区域重叠。

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

(3)死锁是个相当困难的问题,因为它不总是可预测的。

代码实践

count.c

运行结果:

结果分析:截图中可以看出,新线程的ID是在不断转换的,其中一部分为:acfbd700(...1927)——ad7be700(26574040)——acfbd700(19282431)——ad7be700(4041...),可以看出,创建的两个线程共享同一变量都实现加一操作的程序,结果的互相覆盖,最终输出值不是10000,而是5000。

convdar.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
", 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
", 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;
}

运行结果:

结果分析:这个程序展示的是生产者-消费者问题,消费者等待生产者产出产品后才打印,否则消费者阻塞等待,两者共用一个有限缓冲区。wait函数用于等待信号,signal函数用于通知信号。

·线程间同步的一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。
·在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。
·wait函数中condtion是和mutext一起使用的,基本流程如下:
消费者获取资源锁,如果当前无可用资源则调用cond_wait函数释放锁,并等待condtion通知。
生产者产生资源后,获取资源锁,放置资源后嗲用cond_signal函数通知。并释放资源锁。
消费者的cond_wait函数等到condtion通知后,重新获取资源锁,消费资源后再次释放资源锁。

counttwithmutex.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 
", (unsigned int) pthread_self(), val + 1);
        counter = val + 1;
        pthread_mutex_unlock( &counter_mutex );
    }
    return NULL;
}

运行结果:

结果分析:这次的运行结果和我们期望的就是一样的,因此对于多线程的程序,访问冲突的问题是很普遍的,解决的办法就是引入互斥锁(Mutex),获得锁的线程可以完成”读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样”读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。

cp_t.c

运行结果:

createthread.c

结果分析:这个程序打印了进程和线程的tid

semphore.c

运行结果:

结果分析:semaphore表示信号量,semaphore变量的类型为sem_t,sem_init()初始化一个semaphore变量,value参数表示可用资源 的数量,pshared参数为0表示信号量用于同一进程的线程间同步。在用完semaphore变量之后应该调用sem_destroy()释放与semaphore相关的资源。调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已 经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()。调用sem_post()可以释放资 源,使semaphore的值加1,同时唤醒挂起等待的线程。

share.c

运行结果:

结果分析:该代码主要是为了获得线程的终止状态,thr_fn 1,thr_fn 2和thr_fn 3三个函数对应终止线程的三种方法,即从线程函数return,调用pthread_exit终止自己和调用pthread_cancel终止同一进程中的另一个线程。

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

本周的运行代码涉及到多线程,所以在编译代码的时候在末尾要加上-lpthread

- 本周代码托管链接

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

这一周的代码我感觉需要花很多时间去理解,有些代码一开始确实是搞不懂到底是想干什么,卢肖明同学的博客是非常好的学习参考资料。虽然还有很多不理解的地方,再加上自身的能力有待提高,因此我还是会坚持自己动手将代码都运行一遍,再结合学霸们的博客进行分析。

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 3000行 20篇 200小时
第0周 0/0 1/1 5/5
第1周 0/0 1/2 10/15
第2周 300/300 1/3 15/30
第3周 200/500 1/4 10/40
第5周 150/650 1/5 10/50
第6周 50/700 1/6 8/58
第7周 0/700 1/7 8/64
第8周 0/700 2/9 5/70
第9周 181/881 2/11 7/77
第10周 0/881 2/11 5/52
第11周 1017/1898 2/13 7/59
第12周 0/1898 3/16 5/64
第13周 421/2339 1/17 5/69

参考资料

原文地址:https://www.cnblogs.com/20145223ymy/p/6159556.html