20145330 《信息安全系统设计基础》第13周学习总结
第十一章 网络编程
11.1客户端-服务端编程模型
- 一个应用是由一个服务器进程和一个或者多个客户端进程组成的
- 客户端和服务端是进程
11.2 网络
- 对于一个主机而言,网络只是又一种I/O设备
- 物理上而言,网络是一个按照地理远近组成的层次系统。
- 最底层是LAN(局域网)
- 适配器提供到网络的物理接口
- 以太网段(电缆+集线器)
- 一台主机可以发送一段位,称为帧
- 每个主机适配器都能看到这个帧,但是只有目的主机实际读取它
- 多个以太网段可以连接成较大的局域网,称为桥接以太网
- 网桥比集线器更充分的利用了电缆带宽
- 互联网络
- 在层次较高的级别中,多个不兼容的局域网可以通过路由器连接起来,组成互联网络
- 每台路由器对于它所连接到的每个网络都有一个适配器(端口)
- WAN(广域网)
- 路由器可以用来由各种局域网和广域网构建互联网络
- 网络协议提供两种基本能力:
- 命名机制
- 传送机制
封装是关键
11.3 全球IP因特网
- 因特网的客户端和服务端混合使用套接字接口函数和Unix I/O函数来进行通信
- 因特网上的主机通过IP地址和域名来标识
- TCP/IP实际上是一个协议族
- IP机制从某种意义上而言是不可靠的
- TCP是一个构建在IP之上的复杂协议,提供了进程间可靠地全双工连接
- 11.3.1 IP地址
- 一个IP地址就是一个32位无符号整数
- IP/TCP为任意整数数据项定义了统一的网络字节顺序(大端字节顺序)
- 对inet-aton的调用传递的是指向结构的指针,而对inet_ntoa的调用传递的是结构本身
- 11.3.2 因特网域名
- 域名集合形成一个层次结构,子树称为子域
- 一个IP对多个域名,可供多个域名解析
- 但域名解析到的地址是一个对一个
使用HOSTNAME命令来确定自己主机的点分十进制:
使用HOSTNAME命令来确定自己主机的实际域名:
- 某些合法域名没有映射到任何IP地址
- 11.3.3 因特网连接
- 点对点、全双工、可靠
- 客户端套接字地址中的端口是由内核自动分配的,称为临时端口
- 服务端套接字地址中的端口通常是某个知名端口(HTTP:80)
套接字地址:(地址:端口)
11.4套接字接口
- 套接字接口是一组函数,用以创建网络应用
- 套接字地址结构
- sin_family成员是AF_INET
- sin_port成员是一个16位端口
- sin_addr成员是32位的IP地址
- IP地址和端口号总是以网络字节顺序(大端法)存放的
- socket函数
- 客户端和服务端使用socket函数来创建一个套接字描述符
- connect函数
- 建立和服务器的连接。
- open_clientfd函数
- 将socket和connect函数包装而成。客户端可以用它来和服务器建立连接。
- bind函数
- listen函数
- accept函数
- 均被服务器用于和客户端建立连接。
- open_listenfd函数
socket、bind和listen函数结合。用于服务器创建一个监听描述符。
Web服务器
- (1)协议
- Web 客户端和服务器之间的交互用的是一个基于文本的应用级协议,叫做 HTTP (超文本传输协议).
- HTTP 是一个简单的协议。
- 一个 Web 客户端(即浏览器) 打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请求的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上。
- Web内容可以用一种叫做 HTML(Hypertext Markup Language,超文本标记语言)的语言来编写。一个 HTML 程序(页)包含指令(标记),它们告诉浏览器如何显示这页中的各种文本和图形对象。
- (2)内容
- 对于Web客户端和服务端而言,内容是与一个MIME类型相关的字节序列
- Web 服务器以两种不同的方式向客户端提供内容:
- 取一个磁盘文件,并将它的内容返回给客户端。磁盘文件称为静态内容 , 而返回文件给客户端的过程称为服务静态内容
- 运行一个可执行文件,并将它的输出返回给客户端。运行时可执行文件产生的输出称为态内容 ,而运行程序并返回它的输出到客户端的过程称为服务动态内容
- 每条由Web服务器返回的内容都是和他管理的某个文件相关联的。这些文件每一个都有一个唯一的名字,叫做:URL
第12章 并发程序
- 使用应用级并发的应用程序称为并发程序。现代操作系统提供了三种基本的构造并发程序的方法:
- 进程
- I/O多路复用
线程
12.1 基于进程的并发进程
- 构造并发程序最简单的方法就是用进程
- 第一步:服务器接受客户端的连接请求
- 第二步:服务器派生一个子进程为这个客户端服务
- 第三步:服务器接受另一个连接请求
- 第四步:服务器派生另一个子进程为新的客户端服务
- 12.1.1 基于进程的并发服务器
- 首先,包括一个SIGCHLD处理程序,回收僵死子进程资源
- 其次,父子进程必须关闭它们各自的connfd拷贝,以免存储器泄露
- 最后,知道父子进程的connfd都关闭了,到客户端的连接才会终止
- 12.1.2 关于进程的优劣
- 父子进程间共享状态信息,共享文件表,但是不共享用户地址空间。
- 进程有独立的地址空间(既是优点也是缺点)
- 优点:一个进程不可能不小心覆盖另一个进程的虚拟存储器
缺点:独立的地址空间使得进程共享信息变得更加困难;IPC机制往往比较慢
12.2 基于I/O多路复用的并发进程
- 基本思路就是使用select函数,要求内核挂起进程,在I/O事件发生后,才将控制返回给应用程序。
- select函数处理类型为fd_set的集合,也叫作描述符集合
- 只允许对描述符集合做三件事:
- 分配
- 将一个此种类型的变量赋值给另一个变量
- 用FD_ZERO、FD_SET等宏指令来修改和检查它们
- 只允许对描述符集合做三件事:
- 12.2.1 基于I/O多路复用的并发事件驱动服务器
- 一个状态机就是:状态、输入事件和转移(状态机sk)
- 状态:等待描述符dk准备好可读
- 输入事件:描述符dk准备好可以读了
- 转移:从描述符dk读一个文本行
- 自循环是同一输入和输出状态之间的转移
- init pool:初始化活动客户端池
- clientfd 数组表示已连接描述符的集合, 其中整数 -1 表示一个可用的槽位。初始时,已连接描述符集合是空的,而且监听描述符是 select 读集合中唯一的描述符。
- add_client:向池中添加一个新的客户端连接
- select 函数检测到输入事件,而 add_client 函数创建 一个新的逻辑流(状态机)。
- check_clients:为准备好的客户端连接服务
- 一个状态机就是:状态、输入事件和转移(状态机sk)
- 12.2.2 I/O多路复用技术的优劣
- 优点:它比基于进程的设计给了程序员更多的对程序行为的控制
- 它是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间
缺点:编码复杂
12.3 基于线程的并发编程
- 线程就是运行在进程上下文的逻辑流,由内核进行调度
- 每个线程都有它自己的线程上下文,包括一个唯一的整数线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码
- 所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间
- 12.3.1 线程执行模型
- 主线程:每个进程开始生命周期时第一个运行的线程
- 对等线程:某时刻主线程创建的
- 线程的上下文切换要比进程的上下文切换快得多。
- 和一个进程相关的线程组成一个对等(线程)池 (pool),独立于其他线程创建的线程。
- 线程不像进程一样,不是按照严格的父子进程组织的
- 每个对等线程都能读写相同共享数据
- 12.3.2 Posix线程
- Posix线程是在C程序中处理线程的一个标准接口
- Pthread允许程序创建、杀死和回收线程,与对等线程安全的共享数据,还可以通知对等线程系统状态的变化
- 现成的代码和本地数据被封装在一个线程例程中
- 12.3.3 创建线程
- 线程通过调用pthread_create函数来创建其他线程
- 新线程可以通过调用pthread_self函数来获得它自己的线程ID
- 12.3.4 终止线程
- 当顶层的线程例程返回时,线程会隐式的终止
- 通过调用pthread_exit函数,线程会显示的zhongzhi
- 某个对等线程调用Unix的exit函数,该函数终止进程以及所有与该进程相关的线程
- 另一个对等线程通过调用pthread_cancle函数来终止当前线程
- 12.3.5 回收已终止线程的资源
- 线程通过调用pthread_join函数等待其他线程终止
- 与wait函数不同,pthread_join函数只能等待一个指定的线程终止
- 12.3.6 分离线程
- 一个分离的线程是不能被其他线程回收或杀死的。它的存储器资源在它终止时由系统自动释放
- 每个可结合线程都应该要么被其他线程显示的收回,要么通过调用pthread_detach函数被分离
- pthreaddetach 函数分离可结合线程 tid. 线程能够通过以 pthreadself()为参数的 pthread_detach 调用来分离它们自己。
- 12.3.7 初始化线程
- pthread_once函数允许你初始化与线程例程相关的状态
- once_control变量是一个全局或者静态变量
- 12.3.8 一个基于线程的并发服务器
- 主线程不断地等待连接请求,然后创建一个对等线程处理该要求
- 为了避免潜在的致命竞争,将每个accept返回的已连接描述符分配到它自己的动态分配的存储器块
- 另一个问题是在线程例程中避免存储器泄露
调用 pthread_ create 时,如何将已连接描述符传递给对等线程。最明显的方法就是传递一个指向这个描述符的指针。 对等线程间接引用这个指针,并将它赋值给一个局部变量。
12.4 多线程程序中的共享变量
- 12.4.1 线程存储器模型
- 每个线程都有它自己独立的线程上下文,每个线程和其他线程一起共享进程上下文的剩余部分
- 寄存器是从不共享的,而虚拟存储器总是共享的
- 如果一个线程以某种方式得到一个指向其他线程栈的指针,那么它就可以读写这个栈的任何部分
- 12.4.2 将变量映射到存储器(根据存储类型)
- 全局变量
- 全局变量是定义在函数之外的变量
- 本地自动变量
- 本地自动变量就是定义在函数内部但是没有static属性的变量
- 本地静态变量
- 本地静态变量是定义在函数内部有static属性的变量
- 全局变量
- 12.4.3 共享变量
- 一个变量是共享的,当且仅当它的一个实例被一个以上线程引用
- myid不是共享的,因为它的两个实例中每一个都只被一个线程引用
msgs这样的本地自动变量也能被共享
12.5 用信号量同步线程
进度图
进度图是将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,原点对应于没有任何线程完成一条指令的初始状态。
- 当n=2时,状态比较简单,是比较熟悉的二维坐标图,横纵坐标各代表一个线程,而转换被表示为有向边
- 转换规则:
- 合法的转换是向右或者向上,即某一个线程中的一条指令完成
- 两条指令不能在同一时刻完成,即不允许出现对角线
- 程序不能反向运行,即不能出现向下或向左
而一个程序的执行历史被模型化为状态空间中的一条轨迹线。
线程循环代码的分解: H:在循环头部的指令块 L:加载共享变量cnt到线程i中寄存器%eax的指令。 U:更新(增加)%eax的指令 S:将%eax的更新值存回到共享变量cnt的指令 T:循环尾部的指令块- 临界区:对于线程i,操作共享变量cnt内容的指令L,U,S构成了一个关于共享变量cnt的临界区。
- 不安全区:两个临界区的交集形成的状态
- 安全轨迹线:绕开不安全区的轨迹线
- 信号量
- 信号量是具有非负整数值的全局变量,只能由两种特殊的操作来处理,这两种操作称为P和V
- P(s):如果s是非零的,那么P将s-1,并且立即返回;如果s为零,那么就挂起这个线程,直到s变为非零
- V(s):V操作将s+1
- 当有多个线程在等待同一个信号量时,你不能预测V操作要重启哪一个线程。
- 信号量不变性:一个正在运行的程序绝不能进入这样一种状态,也就是一个正确初始化了的信号量有一个负值。
- 信号量是具有非负整数值的全局变量,只能由两种特殊的操作来处理,这两种操作称为P和V
- 使用信号量来实现互斥
- 二元信号量:将每个共享变量与一个信号量联系起来,然后用然后用P(S)和V(s)操作将这种临界区包围起来,这种方式来保护共享变量的信号量。
- 互斥锁:以提供互斥为目的的二元信号量
- 加锁:一个互斥锁上执行P操作称为对互斥锁加锁,执行V操作称为对互斥锁解锁。对一个互斥锁加了锁但还没有解锁的线程称为占用了这个互斥锁。
- 计数信号量:一个呗用作一组可用资源的计数器的信号量
代码调试中的问题和解决过程
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的释放和重新获取操作,因此生产者和消费者并不会出现死锁。
- 注意:gcc编译的时候要加上-lpthread选项
count
#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;
}
}
- 这是一个不加锁的创建两个线程共享同一变量都实现加一操作的程序.
createthread
#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
本周代码托管截图
感悟与思考
这周的学习内容很丰富,需要化整为零每天学习一点,已经到了临近期末的时候,学习更加不能松懈,就像跑八百已经完成了六百米那剩下的两百米咬着牙跑完才能算完成,否则功亏一篑。希望可以坚持完成好最后几周的学习任务。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 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 | |
第五周 | 50/1350 | 2/11 | 20/120 | |
第六周 | 100/1450 | 1/12 | 20/140 | |
第七周 | 50/1500 | 1/13 | 20/160 | |
第八周 | 000/1500 | 2/15 | 30/190 | |
第九周 | 50/1550 | 2/17 | 30/220 | |
第十周 | 400/2050 | 2/19 | 30/280 | |
第11周 | 500/2550 | 2/21 | 30/310 | |
第12周 | 000/2550 | 2/23 | 30/340 | |
第13周 | 500/3050 | 1/24 | 30/370 |