20145231《信息安全系统设计基础》第13周学习总结
教材学习内容总结
并发编程
程序级并发——进程
函数级并发——线程
三种基本的构造并发程序的方法:
1、进程:每个逻辑控制流是一个进程,由内核进行调度,进程有独立的虚拟地址空间
2、I/O多路复用:逻辑流被模型化为状态机,所有流共享同一个地址空间
3、线程:运行在单一进程上下文中的逻辑流,由内核进行调度,共享同一个虚拟地址空间
基于进程的并发编程
构造并发程序最简单的方法——用进程。常用函数如下:fork,exec,waitpid
构造并发服务器:在父进程中接受客户端连接请求,然后创建一个新的子进程来为每个新客户端提供服务。
需要注意的事情:
1.父进程需要关闭它的已连接描述符的拷贝(子进程也需要关闭)
2.必须要包括一个SIGCHLD处理程序来回收僵死子进程的资源
3.父子进程之间共享文件表,但是不共享用户地址空间
独立地址空间的优点是防止虚拟存储器被错误覆盖,缺点是开销高,共享状态信息才需要IPC机制
基于I/O多路复用的并发编程
I/O多路复用技术使用select函数要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
select函数处理类型为fd_set的集合,即描述符集合,并在逻辑上描述为一个大小为n的位向量,每一位b[k]对应描述符k,但当且仅当b[k]=1,描述符k才表明是描述符集合的一个元素。
描述符能做的三件事:
1、分配他们
2、将一个此种类型的变量赋值给另一个变量
3、用FD_ZERO、FD_SET、FD_CLR和FD_ISSET宏指令来修改和检查它们
当且仅当一个从该描述符读取一个字节的请求不会阻塞时,描述符k表示准备好可以读了
我们必须在每次调用select时都更新读集合
事件驱动程序:将逻辑流模型化为状态机。
一个状态机就是一组状态、输入事件和转移,其中转移就是将状态和输入事件映射到状态。
基于I/O多路复用的并发事件驱动服务器的流程如下:
•select函数检测到输入事件
•add_client函数创建新状态机
•check_clients函数执行状态转移(在课本的例题中是回送输入行),并且完成时删除该状态机。
基于线程的并发编程
线程:就是运行在进程上下文中的逻辑流。
线程由内核自动调度。每个线程都有它自己的线程上下文:
•一个唯一的整数线程ID——TID
•栈
•栈指针
•程序计数器
•通用目的寄存器
•条件码
线程执行模型
在每个进程开始生命周期时都是单一线程——主线程,与其他进程的区别仅有:它总是进程中第一个运行的线程。
对等线程是某时刻主线程创建,之后两个线程并发运行每个对等线程都能读写相同的共享数据。
主线程切换到对等线程的方式是上下文切换,对等线程执行一段时间后会控制传递回主线程,以此类推,切换的原因是:
•主线程执行一个慢速系统调用,如read或sleep
•被系统的间隔计时器中断
线程和进程的区别:
•线程的上下文切换比进程快得多
•组织形式:
•进程:严格的父子层次
•线程:一个进程相关线程组成对等(线程)池,和其他进程的线程独立开来。一个线程可以杀死它的任意对等线程,或者等待他的任意对等线程终止。
Posix线程
Posix线程是C程序中处理线程的一个标准接口。基本用法是:
•线程的代码和本地数据被封装在一个线程例程中
•每个线程例程都以一个通用指针为输入,并返回一个通用指针。
创建线程
创建线程:pthread_create函数,返回时参数tid包含新创建线程的ID
查看线程ID:pthread_self函数,返回调用者的线程ID(TID)
终止线程
终止线程的几个方式:
•隐式终止:顶层的线程例程返回
•显示终止:调用pthread_exit函数
*如果主线程调用,会先等待所有其他对等线程终止,再终止主线程和整个进程,返回值为pthread_return
•某个对等线程调用Unix的exit函数,会终止进程与其相关线程
•另一个对等线程通过以当前线程ID作为参数调用pthread_cancle来终止当前线程
回收已终止线程的资源
pthread_join函数等待其他线程终止
这个函数会阻塞,知道线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源
分离线程
在任何一个时间点上,线程是可结合的,或是分离的。
可结合的线程
•能够被其他线程收回其资源和杀死
•被收回钱,它的存储器资源没有被释放
•每个可结合线程要么被其他线程显式的收回,要么通过调用pthread_detach函数被分离
分离的线程
•不能被其他线程回收或杀死
•存储器资源在它终止时由系统自动释放
pthread_detach函数可以分离可结合线程tid。
线程能够通过以pthread_self()为参数的pthread_detach调用来分离他们自己。
每个对等线程都应该在他开始处理请求之前分离他自身,以使得系统能在它终止后回收它的存储器资源。
初始化线程
pthread_once函数允许你初始化与线程例程相关的状态,总是返回0.
一个基于线程的并发服务器
调用pthread_create时,用传递指针的方法将已连接描述符传递给对等进程
避免存储器泄露,必须分离每个线程,使它终止时它的存储器资源能被收回。
多线程程序中的共享变量
一个变量是共享的,当且仅当多个线程引用这个变量的某个实例
线程存储器模型
寄存器从不共享,虚拟存储器总是共享的
将变量映射到存储器
共享变量
变量v是共享的——当且仅当它的一个实例被一个以上的线程引用
用信号量同步线程
一般而言,没有办法预测操作系统是否将为你的线程选择一个正确的顺序。
进度图
进度图是将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,原点对应于没有任何线程完成一条指令的初始状态。
当n=2时,状态比较简单,是比较熟悉的二维坐标图,横纵坐标各代表一个线程,而转换被表示为有向边
转换规则:
•合法的转换是向右或者向上,即某一个线程中的一条指令完成
•两条指令不能在同一时刻完成,即不允许出现对角线
•程序不能反向运行,即不能出现向下或向左
而一个程序的执行历史被模型化为状态空间中的一条轨迹线。
线程循环代码的分解:
•H:在循环头部的指令块
•L:加载共享变量cnt到线程i中寄存器%eax的指令。
•U:更新(增加)%eax的指令
•S:将%eax的更新值存回到共享变量cnt的指令
•T:循环尾部的指令块
几个概念:
•临界区:对于线程i,操作共享变量cnt内容的指令L,U,S构成了一个关于共享变量cnt的临界区。
•不安全区:两个临界区的交集形成的状态
•安全轨迹线:绕开不安全区的轨迹线
信号量
信号量实现互斥的基本原理
•两个或多个进程通过传递信号进行合作,可以迫使进程在某个位置暂时停止执行(阻塞等待),直到它收到一个可以“向前推进”的信号(被唤醒);
•将实现信号灯作用的变量称为信号量,常定义为记录型变量s,其一个域为整型,另一个域为队列,其元素为等待该信号量的阻塞进程(FIFO)。
•信号量定义:
type semaphore=record
count: integer;
queue: list of process
end;
var s:semaphore;
定义对信号量的两个原子操作——P和V
P(wait)
wait(s)
s.count :=s.count-1;
if s.count<0 then
begin
进程阻塞;
进程进入s.queue队列;
end;
V(signal)
signal(s)
s.count :=s.count+1;
if s.count ≤0 then
begin
唤醒队首进程;
将进程从s.queue阻塞队列中移出;
end;
需要注意的是,每个信号量在使用前必须初始化。
使用信号量来实现互斥
基本思想是将每个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P和V操作将相应的临界区包围起来。
几个概念
•二元信号量:用这种方式来保护共享变量的信号量叫做二元信号量,取值总是0或者1.
•互斥锁:以提供互斥为目的的二元信号量
•加锁:对一个互斥锁执行P操作
•解锁;对一个互斥锁执行V操作
•计数信号量:被用作一组可用资源的计数器的信号量
•禁止区:由于信号量的不变性,没有实际可能的轨迹能够包含禁止区中的状态。
利用信号量来调度共享资源
信号量有两个作用:实现互斥;调度共享资源
信号量分为:互斥信号量和资源信号量。
互斥信号量用于申请或释放资源的使用权,常初始化为1;
资源信号量用于申请或归还资源,可以初始化为大于1的正整数,表示系统中某类资源的可用个数。
常见问题有生产者-消费者问题,和读者-写者问题
其他并发问题
一个线程是安全的,当且仅当被多个并发线程反复的调用时,它会一直产生正确的结果。
四个不相交的线程不安全函数类以及应对措施:
•不保护共享变量的函数——用P和V这样的同步操作保护共享变量
•保持跨越多个调用的状态的函数——重写,不用任何static数据。
•返回指向静态变量的指针的函数——①重写;②使用加锁-拷贝技术。
•调用线程不安全函数的函数——参考之前三种
可重入性
当它们被多个线程调用时,不会引用任何共享数据。
显式可重入的:所有函数参数都是传值传递,没有指针,并且所有的数据引用都是本地的自动栈变量,没有引用静态或全剧变量。
隐式可重入的:调用线程小心的传递指向非共享数据的指针。
在线程化的程序中使用已存在的库函数
一句话,就是使用线程不安全函数的可重入版本,名字以_r为后缀结尾。
竞争
竞争发生的原因:一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点。也就是说,程序员假定线程会按照某种特殊的轨迹穿过执行状态空间,忘了一条准则规定:线程化的程序必须对任何可行的轨迹线都正确工作。
消除方法:动态的为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针
死锁
一组线程被阻塞了,等待一个永远也不会为真的条件。
实践学习总结
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
", 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;
}
运行结果:
消费者等待生产者产出产品后才打印,否则消费者阻塞等待生产者生产。
线程间同步的一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。
在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。
wait函数中condtion是和mutext一起使用的,基本流程如下:
消费者获取资源锁,如果当前无可用资源则调用cond_wait函数释放锁,并等待condtion通知。
生产者产生资源后,获取资源锁,放置资源后嗲用cond_signal函数通知。并释放资源锁。
消费者的cond_wait函数等到condtion通知后,重新获取资源锁,消费资源后再次释放资源锁。
从代码中可以看到,mutex用于保护资源,wait函数用于等待信号,signal函数用于通知信号。其中wait函数中有一次对mutex的释放和重新获取操作,因此生产者和消费者并不会出现死锁。
注意:gcc编译的时候要加上-lpthread选项
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
", (unsigned int) pthread_self(), val + 1);
counter = val + 1;
}
}
运行结果:
这是一个不加锁的创建两个线程共享同一变量都实现加一操作的程序,在这个程序中虽然每个线程都给count加了5000,但由于结果的互相覆盖,最终输出值不是10000,而是5000。
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
", (unsigned int) pthread_self(), val + 1);
counter = val + 1;
pthread_mutex_unlock( &counter_mutex );
}
return NULL;
}
运行结果:
这次的运行结果和我们期望的就是一样的,因此对于多线程的程序,访问冲突的问题是很普遍的,解决的办法就是引入互斥锁(Mutex),获得锁的线程可以完成”读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样”读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
cp_t.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <pthread.h>
typedef struct {
char *src_address;
char *dest_address;
int len;
} arg_t;
static void err_sys(const char *s);
static int get_file_length(int fd);
static int extend(int fd, int len);
arg_t map_src_dest(const char *src, const char *dest);
static void *cpy(void *arg);
void divide_thread(int pnum, arg_t arg_file);
int main(int argc, char *argv[])
{
arg_t arg_file;
if (argc != 4) {
fprintf(stderr, "Usage:%s file1 file2 thread_num", argv[0]);
exit(1);
}
arg_file = map_src_dest(argv[1], argv[2]);
divide_thread(atoi(argv[3]), arg_file);
munmap(arg_file.src_address, arg_file.len);
munmap(arg_file.dest_address, arg_file.len);
return 0;
}
static void err_sys(const char *s)
{
perror(s);
exit(1);
}
static int get_file_length(int fd)
{
int position = lseek(fd, 0, SEEK_CUR);
int length = lseek(fd, 0, SEEK_END);
lseek(fd, position, SEEK_SET);
return length;
}
static int extend(int fd, int len)
{
int file_len = get_file_length(fd);
if (file_len >= len) {
return -1;
}
lseek(fd, len - file_len - 1, SEEK_END);
write(fd, "", 1);
return 0;
}
arg_t map_src_dest(const char *src, const char *dest)
{
int fd_src, fd_dest, len;
char *src_address, *dest_address;
arg_t arg_file;
fd_src = open(src, O_RDONLY);
if (fd_src < 0) {
err_sys("open src");
}
len = get_file_length(fd_src);
src_address = mmap(NULL, len, PROT_READ, MAP_SHARED, fd_src, 0);
if (src_address == MAP_FAILED) {
err_sys("mmap src");
}
close(fd_src);
fd_dest = open(dest, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd_dest < 0) {
err_sys("open dest");
}
extend(fd_dest, len);
dest_address = mmap(NULL, len, PROT_WRITE, MAP_SHARED, fd_dest, 0);
if (dest_address == MAP_FAILED) {
err_sys("mmap dest");
}
close(fd_dest);
arg_file.len = len;
arg_file.src_address = src_address;
arg_file.dest_address = dest_address;
return arg_file;
}
static void *cpy(void *arg)
{
char *src_address, *dest_address;
int len;
src_address = ((arg_t *) arg)->src_address;
dest_address = ((arg_t *) arg)->dest_address;
len = ((arg_t *) arg)->len;
memcpy(dest_address, src_address, len);
return NULL;
}
void divide_thread(int pnum, arg_t arg_file)
{
int i, len;
char *src_address, *dest_address;
pthread_t *pid;
arg_t arg[pnum];
len = arg_file.len;
src_address = arg_file.src_address;
dest_address = arg_file.dest_address;
pid = malloc(pnum * sizeof(pid));
if (pnum > len) {
fprintf(stderr,
"too many threads, even larger than length, are you crazy?!
");
exit(1);
}
for (i = 0; i < pnum; i++) {
arg[i].src_address = src_address + len / pnum * i;
arg[i].dest_address = dest_address + len / pnum * i;
if (i != pnum - 1) {
arg[i].len = len / pnum;
} else {
arg[i].len = len - len / pnum * i;
}
pthread_create(&pid[i], NULL, cpy, &arg[i]);
}
for (i = 0; i < pnum; i++) {
pthread_join(pid[i], NULL);
}
free(pid);
}
运行结果:
用法:./cp_t [源文件名] [目的文件名] [创建线程数]
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)
", 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
", strerror( err ) );
exit( 1 );
}
printids( "main threads: " );
sleep(1);
return 0;
}
运行结果:
这个程序作用是打印进程和线程ID,查看pthread_create函数的帮助文档:
pthread_create函数中四个参数的意思:
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
函数thr_fn不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
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
", 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
", 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;
}
运行结果:
查看sem_init帮助文档:
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
#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
", getpid());
printf("pwd:%s
", getcwd(buf, BUFSIZ));
*(int *)arg = 11;
return (void *) 1;
}
void *thr_fn2( void *arg )
{
printf("thread 2 returning %d
", getpid());
printf("pwd:%s
", getcwd(buf, BUFSIZ));
pthread_exit( (void *) 2 );
}
void *thr_fn3( void *arg )
{
while( 1 ){
printf("thread 3 writing %d
", getpid());
printf("pwd:%s
", 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 );
printf("thread 1 exit code %d
", (int) tret );
pthread_create( &tid, NULL, thr_fn2, NULL);
pthread_join( tid, &tret );
printf("thread 2 exit code %d
", (int) tret );
pthread_create( &tid, NULL, thr_fn3, NULL);
sleep( 3 );
pthread_cancel(tid);
pthread_join( tid, &tret );
printf("thread 3 exit code %d
", (int) tret );
}
运行结果:
学习过程中的问题和解决过程
教材学习中遇到的问题
代码还需继续理解
实践中遇到的问题
一开始编译时出错,加上-lpthread选项之后解决。
代码托管截图
托管链接:(https://git.oschina.net/xzhkuma1128/CSAPP2E)
心得体会
学习了网络编程和并发编程,网络编程因为是另一门课的内容,所以学习起来比较轻松,而并发编程这一章就比较抽象,需要结合代码运行并分析,同时这周还需要看家庭作业中的内容,所以任务比较重,需要再继续努力去理解这一部分内容。
学习进度条
博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|
目标 | 30篇 | 400小时 | |
第一周 | 1/1 | 20/20 | 学习了Linux核心命令 |
第二周 | 1/2 | 21/41 | 学习了vim、gcc、gdb指令 |
第三周 | 1/3 | 20/61 | 学习了信息的表示和处理,了解了二进制在计算机系统中的重要性 |
第五周 | 1/4 | 20/81 | 学习了机器级程序,读懂汇编代码 |
第六周 | 1/5 | 19/100 | 了解了处理器对于指令的处理过程 |
第七周 | 1/6 | 18/118 | 了解了存储器层次结构及存储技术 |
第八周 | 2/8 | 15/133 | 对前几周内容进行复习 |
第十周 | 3/12 | 10/148 | 学习Linux重要命令 |
第十一周 | 2/14 | 15/163 | 学习异常控制相关知识 |
第十二周 | 2/16 | 10/173 | 复习前几周代码、实验代码 |
第十三周 | 1/17 | 20/193 | 学习了网络编程和并发编程及相关代码 |