20135203齐岳 信息安全系统设计基础第十三周学习总结
学习计时:8/9共小时(计划/实际)
-
读书:4/5
-
代码:1/1
-
作业:1/1
-
博客:2/2
第十二章 并发编程
一、学习目标
- 掌握三种并发的方式:进程、线程、I/O多路复用
- 掌握线程控制及相关系统调用
- 掌握线程同步互斥及相关系统调用
二、学习任务
- 阅读教材,完成课后练习(书中有参考答案)
- 考核:练习题把数据变换一下
- 加分题:课后作业最多两人一组,互相不能重复,1星题目每人最多加一分,2星题目每人最多加二分,3星题目每人最多加三分,4星题目每人最多加四分。
三、学习过程
如果逻辑控制流在时间上重叠,那么他们就是并发的。应用级并发在以下情况中发挥作用:
- 访问慢速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),给所有的锁分配一个全序,每个线程按照这个顺序来请求锁,并且按照逆序来释放,这个程序就是无死锁的。
参考资料
1.《深入理解计算机系统》
2.《计算机操作系统》