2017-2018 20155308 《信息安全系统设计基础》第十四周学习总结
教材第11章详细总结及书上习题
客户端-服务器编程模型
每个网络应用都是基于客户端-服务器模型,一个应用是由一个服务器进程和一个或多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源来为他额客户端提供某种服务。
- 一个客户端-服务器事务由四步组成:
- 当一个客户端需要服务时,向服务器发送一个请求,发起一个事务。
- 服务器收到请求后,解释它,并以适当的方式操作它的资源。
- 服务器给客户端发送一个相应,并等待下一个请求。
- 客户端收到响应并处理它。
网络
- 对主机而言,网络是一种I/O设备,是数据源和数据接收方。
物理上而言,网络是一个按照地理远近组成的层次系统。最低层是LAN(局域网),最流行的局域网技术是以太网。
每个以太网适配器都有一个全球唯一的48位地址,它存储在适配器的非易失性存储器上。一台主机可以发送一段位到这个网段内其它任何主机。每个帧包括一些固定数量的头部位,用来标识此帧的源和目的地址及帧长此后紧随的就是数据位有效载荷。每个主机适配器都能看到这个帧,但是只有目的主机能读取。
- 互联网重要特性是它能由采用不同技术,互不兼容的局域网和广域网组成,并能使其相互通信。
- 协议提供的两种基本能力
- 命名机制:
- 传送机制:
- 主机和路由器使用互连网络协议在不兼容的局域网间传送数据过程的8个基本步骤:
- 运行在主机A上的客户端进行一个系统调用,从客户端的虚拟地址空间复制数据到内核缓冲区中。
- 主机S上的协议软件通过在数据前附加互联网络包头和LAN1帧头,创建一个LAN1的帧。
- LAN1适配器复制该帧到网络上。
- 当此帧到达路由器时,路由器的LAN1适配器从电缆上读取它,并把它传送到协议软件。
- 路由器从互联网络包头中提取出目的互联网络地址,并用它作为路由表的索引,确定向哪里转发这个包。
- 路由器的LAN2适配器复制该帧到网络上。
- 当此帧到达主机B时,它的适配器从电缆上读到此帧,并将它传送到协议软件。
- 最后,主机B上的协议软件剥落包头和帧头。
全球IP因特网
每台因特网主机都运行实现TCP/IP协议,(传输控制协议/互联网协议)的软件,几乎每个现代计算机系统都支持这个协议。
- 把因特网看做一个世界范围的主机集合,满足以下特性:
- 主机集合被映射为一组32位的IP地址。
- 这组IP地址被映射为一组称为因特网域名的标识符。
- 因特网主机上的进程能够通过连接和任何其他主机上的进程。
- 检索并打印一个DNS主机条目:
#include "csapp.h"
int main(int argc, char **argv)
{
char **pp;
struct in_addr addr;
struct hostent *hostp;
if (argc != 2) {
fprintf(stderr, "usage: %s <domain name or dotted-decimal>\\n",
argv[0]);
exit(0);
}
if (inet_aton(argv[1], &addr) != 0)
hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET);
else
hostp = Gethostbyname(argv[1]);
printf("official hostname: %s\\n", hostp->h_name);
for (pp = hostp->h_aliases; *pp != NULL; pp++)
printf("alias: %s\\n", *pp);
for (pp = hostp->h_addr_list; *pp != NULL; pp++) {
addr.s_addr = ((struct in_addr *)*pp)->s_addr;
printf("address: %s\\n", inet_ntoa(addr));
}
exit(0);
}
IP地址
网络程序将IP地址存放在所示IP地址结构中。
在IP地址结构中存放的地址总是一网络字节顺序存放的,即使主机字节顺序是小端法。Unix提供了下面这样的函数在网络和主机字节顺序见时间转换。
应用程序使用以下函数来实现IP地址和点分十进制串之间的转换。
- 练习题11.1
答:
- 练习题11.2
答:代码如下
#include <stdio.h>
int ctoi(char ch)
{
if(ch >= ‘a‘) return (10 + ch -‘a‘);
return ch - ‘0‘;
}
void dex2dd(char *s)
{
int n, flag = 0;
char *p;
p = s + 2;
while(*p) {
n = ctoi(*p) * 16 + ctoi(*++p) * 1;
if(flag > 0) printf(".");
printf("%d", n);
p++; flag++;
}
printf("\\n");
}
int main(int argc,char *argv[])
{
dex2dd(argv[1]);
return 0;
}
- 练习题11.3
答:代码如下
#include <stdio.h>
char a[10];
int ctoi(char ch)
{
if(ch >= ‘a‘) return (10 + ch -‘a‘);
return ch - ‘0‘;
}
//将十进制的正整数n转换成base进制
void decimal(int n, int base)
{
int r, i = 0;
char c;
if(n < 16) printf("0");
do {
r = n % base;
c = r < 10 ? (r + ‘0‘) : (‘a‘ + r - 10);
a[i++] = c;
n = n / base;
} while (n);
while (i) printf("%c", a[--i]);
}
void dd2hex(char *s)
{
char *p, *q;
int i, n, count;
n = 0;
p = q = s;
printf("0x");
while(*q && *p) {
while(*q != ‘.‘ && *q) q++;
count = q - p;
for(i = 0; i < count; i++) {
n = n * 10 + ctoi(*p);
p++;
}
decimal(n, 16);
if(*q) {
q++;
p = q;
}
n = 0;
}
printf("\\n");
}
int main(int argc,char *argv[])
{
dd2hex(argv[1]);
return 0;
}
https://www.tuicool.com/articles/Nbym63R
因特网域名
- 每台因特网主机都有本地定义的域名localhost,这个域名总是映射为回送地址127.0.0.1
- 使用HOSTNAME来确定本地主机的实际域名
- 最简单的情况中,一个域名和一个IP地址之间一一映射
- 在某些情况下,多个域名可以映射为同一个IP地址
- 最通常情况下,多个域名可以映射到同一组的多个IP地址
- 某些合法的域名没有映射到任何IP地址
套接字接口
套接字接口是一组函数,它们和Unix I/O函数结合起来,用于建立网络应用。
?函数: ?socket函数
?connect函数
?open_clientfd函数
?bind函数
?listen函数
?open_listenfd函数
?accept函数
Web服务器
?Web客户端和服务器之间的交互用的是一个基于文本的应用级协议,叫做 HTTP (Hypertext Transfer Protocol,超文本传输协议). HTTP 是一个简单的协议。一个 Web 客户端(即浏览器) 打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请求的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上。
?Web内容可以用一种叫做 html(Hypertext Markup Language,超文本标记语言)的语言来编写。一个 HTML 程序(页)包含指令(标记),它们告诉浏览器如何显示这页中的各种文本和图形对象。
?Web 服务器以两种不同的方式向客户端提供内容: ?取一个磁盘文件,并将它的内容返回给客户端。磁盘文件称为静态内容 (static content), 而返回文件给客户端的过程称为服务静态内容 (serving static content)。
?运行一个可执行文件,并将它的输出返回给客户端。运行时可执行文件产生的输出称为态内容 (dynamic content),而运行程序并返回它的输出到客户端的过程称为服务动态内容 (serving dynamic content)。
并发编程
?并发:逻辑控制流在时间上重叠
?并发程序:使用应用级并发的应用程序称为并发程序。
?三种基本的构造并发程序的方法: ?进程,用内核来调用和维护,有独立的虚拟地址空间,显式的进程间通信机制。
?I/O多路复用,应用程序在一个进程的上下文中显式的调度控制流。逻辑流被模型化为状态机。
?线程,运行在一个单一进程上下文中的逻辑流。由内核进行调度,共享同一个虚拟地址空间。
基于进程的并发编程
?构造并发服务器的自然方法就是,在父进程中接受客户端连接请求,然后创建一个新的子进程来为每个新客户端提供服务。
?因为父子进程中的已连接描述符都指向同一个文件表表项,所以父进程关闭它的已连接描述符的拷贝是至关重要的,而且由此引起的存储器泄露将最终消耗尽可用的存储器,使系统崩溃。
?基于进程的并发echo服务器的重点内容: ?需要一个SIGCHLD处理程序,来回收僵死子进程的资源。
?父子进程必须关闭各自的connfd拷贝。对父进程尤为重要,以避免存储器泄露。
?套接字的文件表表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。
?进程的模型:共享文件表,但不是共享用户地址空间。
?关于进程的优劣: ?优点:一个进程不可能不小心覆盖两一个进程的虚拟存储器。
?缺点:独立的地址空间使得进程共享状态信息变得更加困难。进程控制和IPC的开销很高。
?Unix IPC是指所有允许进程和同一台主机上其他进程进行通信的技术,包括管道、先进先出(FIFO)、系统V共享存储器,以及系统V信号量。
基于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操作要重启哪一个线程。
?信号量不变性:一个正在运行的程序绝不能进入这样一种状态,也就是一个正确初始化了的信号量有一个负值。
使用信号量来实现互斥
?二元信号量:将每个共享变量与一个信号量s联系起来,然后用P(S)和V(s)操作将这种临界区包围起来,这种方式来保护共享变量的信号量。
?互斥锁:以提供互斥为目的的二元信号量
?加锁:一个互斥锁上执行P操作称为对互斥锁加锁,执行V操作称为对互斥锁解锁。对一个互斥锁加了锁但还没有解锁的线程称为占用了这个互斥锁。
?计数信号量:一个呗用作一组可用资源的计数器的信号量
利用信号量来调度共享资源
?信号量的作用:(1)提供互斥(2)调度对共享资源的访问
?生产者—消费者问题:生产者产生项目并把他们插入到一个有限的缓冲区中,消费者从缓冲区中取出这些项目,然后消费它们。
?读者—写者问题: ?读者优先,要求不让读者等待,除非已经把使用对象的权限赋予了一个写者。
?写者优先,要求一旦一个写者准备好可以写,它就会尽可能地完成它的写操作。
?饥饿就是一个线程无限期地阻塞,无法进展。
使用线程提高并行性
?写顺序程序只有一条逻辑流,写并发程序有多条并发流,并行程序是一个运行在多个处理器上的并发程序。并行程序的集合是并发程序集合的真子集。
其他并发问题
线程安全
?线程安全:当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。
?线程不安全:如果一个函数不是线程安全的,就是线程不安全的。
?线程不安全的类: ?不保护共享变量的函数
?保持跨越多个调用的状态的函数。
?返回指向静态变量的指针的函数。解决办法:重写函数和加锁拷贝。
?调用线程不安全函数的函数。
可重入性
?可重入函数:当它们被多个线程调用时,不会引用任何共享数据。可重入函数是线程安全函数的一个真子集 。
?关键思想是我们用一个调用者传递进来的指针取代了静态的next变量。
?显式可重入:没有指针,没有引用静态或全局变量
?隐式可重入:允许它们传递指针
?可重入性即使调用者也是被调用者的属性,并不只是被调用者单独的属性。
在线程化的程序中使用已存在的库函数
?使用线程不安全函数的可重入版本,名字以_r为后缀结尾。
竞争
?竞争:当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点时,就会发生竞争。
?线程化的程序必须对任何可行的轨迹线都正确工作。
?消除方法:动态的为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针
死锁
?死锁:一组线程被阻塞了,等待一个永远也不会为真的条件。
?程序员使用P和V操作不当,以至于两个信号量的禁止区域重叠。
?重叠的禁止区域引起了一组称为死锁区域的状态。
?死锁是不可预测的。