20145218 《信息安全系统设计基础》第十三周学习总结
20145218 《信息安全系统设计基础》第十三周学习总结
教材学习内容总结
- 如果逻辑控制流在时间上重叠,那么他们就是并发的。应用级并发在以下情况中发挥作用:
- 访问慢速I/O设备。
- 与人交互。
- 通过推迟工作以降低延迟。
- 服务多个网络客户端。
- 在多核机器上进行并行计算。
- 使用应用级并发的应用程序称为并发程序。现代操作系统提供了三种基本的构造并发程序的方法:
- 进程。每个逻辑控制流都是一个进程,由内核来调度和维护。控制流使用显式的进程间通信(IPC)机制。
- I/O多路复用。应用程序在一个进程的上下文中显式地调度他们自己的逻辑流。所有的流都共享同一个地址空间。
- 线程。线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。
12.1 基于进程的并发编程
基于进程的并发服务器
- 使用SIGCHLD处理程序来回收僵死子进程的资源。
- 父进程必须关闭他们各自的connfd拷贝(已连接的描述符),避免存储器泄露。
- 因为套接字的文件表表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。
关于进程的优劣
- 在父子进程之间共享状态信息,通过共享文件表,但是不共享用户地址空间。
- 使用显式的进程间通信(IPC)机制。但开销很高,往往比较慢。
基于I/O多路复用的并发编程
- 使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
int select(int n,fd_set *fdset,NULL,NULL,NULL);
返回已经准备好的描述符的非0的个数,若出错则为-1。
- select函数处理类型为fd_set的集合,叫做描述符集合,看做一个大小为n位的向量:
bn-1,......,b1,b0
- 对描述符集合的处理方法:
- 分配他们
- 将一个此种类型的变量赋值给另一个变量
- 用FD_ZERO,FD_SET,FD_CLR和FD_ISSET宏指令来修改和检查他们。
基于I/O多路复用的并发事件驱动服务器
- I/O多路复用可以用作事件并发驱动程序的基础。
- 状态机:一组状态、输入事件、输出事件和转移。
- 自循环:同一输入和输出状态之间的转移。
I/O多路复用技术的优劣
- 相比基于进程的设计给了程序员更多的对进程行为的控制,运行在单一进程上下文中,每个逻辑流都能访问全部的地址空间,在流之间共享数据很容易。
- 编码复杂,随着并发粒度的减小,复杂性还会上升。粒度:每个逻辑流每个时间片执行的指令数量。
12.3 基于线程的并发编程
- 线程:运行在进程上下文中的逻辑流,由内核自动调度,有自己的线程上下文,包括一个唯一的整数线程ID,栈、栈指针、程序计数器、通用目的寄存器和条件码。所有运行在一个进程里的线程共享该进程的整个虚拟地址空间。
线程执行模型
- 每个进程开始生命周期时都是单一线程(主线程),在某一时刻创建一个对等线程,从此开始并发地运行,最后,因为主线程执行一个慢速系统调用,或者被中断,控制就会通过上下文切换传递到对等线程。
Posix线程
- Posix线程是C语言中处理线程的一个标准接口,允许程序创建、杀死和回收线程,与对等线程安全的共享数据。
- 线程的代码和本地数据被封装在一个线程例程中。
创建线程
- 线程通过调用pthread_create来创建其他线程。
int pthread_create(pthread_t *tid,pthread_attr_t *attr,func *f,void *arg);
成功则返回0,出错则为非零
- 当函数返回时,参数tid包含新创建的线程的ID,新线程可以通过调用pthread_self函数来获得自己的线程ID。
pthread_t pthread_self(void);返回调用者的线程ID。
终止线程
- 一个线程是通过以下方式之一来终止的。
- 当顶层的线程例程返回时,线程会隐式地终止。
- 通过调用pthread_exit函数,线程会显式地终止
void pthread_exit(void *thread_return);
回收已终止的线程资源
- 线程通过调用pthread_join函数等待其他线程终止。
int pthread_join(pthread_t tid,void **thread_return);
成功则返回0,出错则为非零
分离线程
- 在任何一个时间点上,线程是可结合或可分离的。一个可结合的线程能够被其他线程收回其资源和杀死,在被回收之前,它的存储器资源是没有被释放的。分离的线程则相反,资源在其终止时自动释放。
int pthread_deacth(pthread_t tid);
成功则返回0,出错则为非零
初始化线程
- pthread_once允许初始化与线程例程相关的状态。
pthread_once_t once_control=PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control,void (*init_routine)(void));
总是返回0
12.4 多线程程序中的共享变量
- 一个变量是共享的。当且仅当多个线程引用这个变量的某个实例。
线程存储器模型
- 每个线程都有自己独立的线程上下文,包括一个唯一的整数线程ID,栈、栈指针、程序计数器、通用目的寄存器和条件码。
- 寄存器是从不共享的,而虚拟存储器总是共享的。
- 各自独立的线程栈被保存在虚拟地址空间的栈区域中,并且通常是被相应的线程独立地访问的。
将变量映射到存储器
- 全局变量:定义在函数之外的变量
- 本地自动变量:定义在函数内部但是没有static属性的变量。
- 本地静态变量:定义在函数内部并有static属性的变量。
共享变量
- 当且仅当变量的一个实例被一个以上的线程引用时,就说变量是共享的。
12.5用信号量同步线程
- 共享变量的同时引入了同步错误,即没有办法预测操作系统是否为线程选择一个正确的顺序。
进度图
- 将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,将指令模型化为从一种状态到另一种状态的转换。
信号量
-
P(s):如果s是非零的,那么P将s减一,并且立即返回。如果s为零,那么就挂起这个线程,直到s变为非零。
-
V(s):将s加一,如果有任何线程阻塞在P操作等待s变为非零,那么V操作会重启线程中的一个,然后该线程将s减一,完成他的P操作。
-
信号量不变性:一个正确初始化了的信号量有一个负值。
-
信号量操作函数:
int sem_init(sem_t *sem,0,unsigned int value);//将信号量初始化为value
int sem_wait(sem_t *s);//P(s)
int sem_post(sem_t *s);//V(s)
使用信号量来实现互斥
- 二元信号量(互斥锁):将每个共享变量与一个信号量s联系起来,然后用P(s)(加锁)和V(s)(解锁)操作将相应的临界区包围起来。
- 禁止区:s<0,因为信号量的不变性,没有实际可行的轨迹线能够直接接触不安全区的部分
12.6 使用线程来提高并行性
- 并行程序的加速比通常定义为:
- 其中,p为处理器核的数量,T为在p个核上的运行时间。
12.7 其他并发问题
线程安全
- 定义四个(不相交的)线程不安全函数类:
- 不保护共享变量的函数。
- 保持跨越多个调用状态的函数。
- 返回指向静态变量指针的函数。
- 调用线程不安全函数的函数。
竞争
- 当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达他的控制流x点时,就会发生竞争。
- 为消除竞争,我么可以动态地为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针。
死锁
- 死锁:一组线程被阻塞了,等待一个永远也不会为真的条件。
- 程序员使用P和V操作顺序不当,以至于两个信号量的禁止区域重叠。
- 重叠的禁止区域引起了一组称为死锁区域的状态。
- 死锁是一个相当难的问题,因为它是不可预测的。
- 互斥锁加锁顺序规则:如果对于程序中每对互斥锁(s,t),给所有的锁分配一个全序,每个线程按照这个顺序来请求锁,并且按照逆序来释放,这个程序就是无死锁的。
教材学习中的问题和解决过程
练习题12.1
第33行代码,父进程关闭了连接描述符后,子进程仍然可以使用该描述符和客户端通信。为什么?
- 当父进程派生子进程时,它得到一个已经连接描述符的副本,并将相关文件表的引用计数从1加到2;父进程关闭它的描述符副本时,引用计数从2减少1(内核不会关闭文件知道它的引用计数变为0)。这样连接仍然保持打开。
练习题12.9
设p表示生产者数量,c表示消费者数量,n表示以项目单元为单位的缓冲区大小。对于下面的美国场景,指出subfinsert和subfremove中的互斥信号量是否是必需的。
A.p =1,c =1,n>1
B.p =1,c=1,n=1
C.p>1,c>1,n=1
- A:是。因为生产者和消费者会并发地访问缓冲区
- B:不是。因为n=1,一个非空的缓冲区就相当于一个满的缓冲区。当缓冲区包含一个项目的时候,生产者就已经被阻塞了;当缓冲区为空的时候,消费者就被阻塞了。所以在任意时刻,只有一个线程可以访问缓冲区,不必加互斥锁。
- C:不是。同上。
代码调试中的问题和解决过程
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的释放和重新获取操作,因此生产者和消费者并不会出现死锁。
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) \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
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\n", getpid());
printf("pwd:%s\n", getcwd(buf, BUFSIZ));
*(int *)arg = 11;
return (void *) 1;
}
void *thr_fn2( void *arg )
{
printf("thread 2 returning %d\n", getpid());
printf("pwd:%s\n", getcwd(buf, BUFSIZ));
pthread_exit( (void *) 2 );
}
void *thr_fn3( void *arg )
{
while( 1 ){
printf("thread 3 writing %d\n", getpid());
printf("pwd:%s\n", 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", n );
printf("thread 1 exit code %d\n", (int) tret );
pthread_create( &tid, NULL, thr_fn2, NULL);
pthread_join( tid, &tret );
printf("thread 2 exit code %d\n", (int) tret );
pthread_create( &tid, NULL, thr_fn3, NULL);
sleep( 3 );
pthread_cancel(tid);
pthread_join( tid, &tret );
printf("thread 3 exit code %d\n", (int) tret );
}
-
运行结果
-
获得线程的终止状态,thr_fn 1,thr_fn 2和thr_fn 3三个函数对应终止线程的三种方法,即从线程函数return,调用pthread_exit终止自己和调用pthread_cancel终止同一进程中的另一个线程。
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 \n", (unsigned int) pthread_self(), val + 1);
counter = val + 1;
pthread_mutex_unlock( &counter_mutex );
}
return NULL;
}
-
运行结果
-
引入互斥锁(Mutex),获得锁的线程可以完成”读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据。
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 \n", 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\n", 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;
}
-
运行结果
-
semaphore表示信号量,semaphore变量的类型为sem_t,sem_init()初始化一个semaphore变量,value参数表示可用资源 的数量,pshared参数为0表示信号量用于同一进程的线程间同步。
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 \n", (unsigned int) pthread_self(), val + 1);
counter = val + 1;
}
}
-
运行结果
-
这是一个不加锁的创建两个线程共享同一变量都实现加一操作的程序,在这个程序中虽然每个线程都给count加了5000,但由于结果的互相覆盖,最终输出值不是10000,而是5000。
本周代码托管截图
代码托管链接:https://git.oschina.net/senlinmilelu/IS20145218
其他(感悟、思考等,可选)
感觉本学期的课程学习过程过得很快,不知不觉之间第十二章的学习也结束了。第十二章的很多内容是和前面联系在一起,现在看来可以理解的知识,如果是在刚开学的时候去读,就未必理解得了,也从一个角度说明慢慢积累的重要性,也可以说“厚积薄发”吧。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 100/100 | 1/2 | 20/20 | |
第二周 | 92/192 | 1/3 | 18/38 | |
第三周 | 195/387 | 1/4 | 22/60 | |
第四周 | 180/567 | 0/4 | 30/90 | |
第五周 | 120/687 | 1/5 | 20/20 | |
第六周 | 130/817 | 1/6 | 18/38 | |
第七周 | 550/1367 | 1/7 | 22/60 | |
第八周 | 0/1367 | 2/9 | 30/90 | |
第九周 | 60/1427 | 2/11 | 20/20 | |
第十周 | 514/1941 | 2/13 | 18/38 | |
第十一周 | 1856/3797 | 2/15 | 22/60 | |
第十二周 | 97/3894 | 2/15 | 22/60 | |
第十三周 | 200/4094 | 1/16 | 22/60 |