并发:两个或多个事件在同一时间间隔内发生;
并行:两个或多个事件在同一时刻发生;
线程是进程当中的一条执行流程。同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程都有独立一套的寄存器和栈,这样可以确保线程的控制流是相对独立的。
优点:
缺点:
多线程编程中一般线程数目大于CPU核心的数目,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片轮转的形式,当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换;
总结:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次在切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换;
信号量:信号量机制广泛使用于进程或线程间的同步或互斥,本质上是一个计数器,分为二值信号量和计数信号量,主要的方法有 sem_init(),sem_wait(),sem_post()。
互斥锁:互斥锁是一种简单的方法来控制对共享资源的访问,互斥锁只有两种状态:加锁(lock)和解锁(unlock);互斥锁具有原子性、唯一性和非繁忙等待三大特性,其中唯一性是指如果一个线程锁定了互斥量,在它没有解锁之前,其他线程无法再对该互斥量加锁;非繁忙等待是指如果一个线程试图去对已经加锁的互斥量进行加锁,那么改线程将被挂起(不占用 CPU 的任何资源),直到该锁被释放,那么该线程将被唤醒并继续执行。
pthread_mutex_init()
pthread_mutex_lock() // 挂起加锁
pthread_mutex_trylock() // 非挂起加锁,线程在锁被占据时返回EBASY的错误,而不是挂起
pthread_mutex_unlock()
pthread_mutex_destroy()
条件变量:条件变量与互斥锁不同,条件变量不是用来加锁的,而是一种等待机制。条件变量用来自动阻塞一个线程,直到相应的某种特殊情况发生为止。条件变量是与互斥锁来配合使用的,条件变量使线程睡眠等待某种条件的出现,利用线程间共享全局变量进行同步。一个线程等待条件成立而挂起,另一个线程使条件成立(给出条件成立的信号)而唤醒其他进程。
条件的检测是在互斥锁的保护下进行的;在检测条件之前,首先锁住互斥量,然后若条件不成立那么线程阻塞,并释放互斥锁,当另一个线程改变了条件,那么将会发送信号给关联的条件变量,唤醒一个或多个等待它的线程。
pthread_cond_init()
pthread_cond_wait() // pthread_cond_wait期间会自动解锁,等待条件触发,然后在返回前自动加锁,这种机制可以保证在线程加锁互斥量和进入等待条件变量期间条件变量不被触发
pthread_cond_singal() // 激活一个等待该条件的线程
pthread_cond_boardcast() // 激活所有等待该条件的线程
pthread_cond_destory()
读写锁:读写锁与互斥锁类似,但它拥有更高的并行性,特点是写独占、读共享。
读写锁的特征:
读写锁非常适合与读的次数远大于写的情况。
自旋锁是互斥锁的一种实现方式,但是对于互斥锁来说加锁失败会放弃 CPU 即处于挂起状态,但是自旋锁会加锁失败会一直占用 CPU,即忙等待(自旋)的状态,不停的循环并且检测锁的状态。
CAS 的原理,与互斥锁的区别,CAS 存在的问题,如何解决
为什么要引入 CAS?CAS 的概念的引入主要是为了解决多线程竞争环境下,使用互斥锁导致的性能损耗的问题。
CAS 包括三个操作数,内存位置 V,期望原值,新值,如果内存位置的值和期望原值相同,那么使用新值进行更新,否则处理器不做作何操作。CAS 用于无锁编程,在没有使用到锁的情况下实现多线程之间变量的同步,即本线程不被阻塞。CAS 可认为是一个非阻塞轻量级的乐观锁。
使用 CAS 模拟互斥锁的行为就是自旋锁,就是当一个线程在获取锁的时候,如果该锁已被其他线程占用,那么它进入循环等待状态,并检测锁是否能够被成功获取,直到获得锁才会退出循环。若获取锁的线程一直处于活跃状态,那么进入自旋的线程会出现忙等待的现象,造成 CPU 的资源浪费。
CAS 与互斥锁的区别:
在 CAS 失败的情况下,自旋锁会一直自旋,但是对于互斥锁来讲,加锁失败将会导致线程挂起,不消耗 CPU 资源,因此,两者的区别就是自旋的时间和线程上下文切换的时间,在资源充足的情况下,自旋会更快一些;但是在竞争很激烈的情况下,可能只有一个 CAS 可以成功,其他都是失败自旋,那么此时 CPU 的资源消耗可以说是巨大的。解决方案的话我们可以设置超时等。
CAS 存在的问题:ABA 问题,两个线程 p1 和 p2,p1 从内存中读到的值是 A,p2 将值改为 B,然后再改回 A,之后又被 p1 抢占,读取到的值还是 A。由于 CAS 判断的是指针的地址,若该地址被重用,那么问题就很大了,针对此问题我们可以采用具有原子性的内存引用计数。
多线程模型有多对一模型、一对一模型和多对多模型。
线程安全就是在多线程并发执行某一区域的代码,不会出现结果不一致的情况。线程安全的问题一般是由于对全局变量、静态变量的操作造成的。
可重入是多个执行流反复执行一段代码,其结果不会受其他执行流的影响而改变,通常是访问各自的栈资源。
可重入函数的定义时某个执行流由于异常或者中断暂时导致正在执行的函数不被执行,转而其他执行流执行该函数,那么后者对同一个函数的执行结果不会影响前者恢复执行后对函数结果的影响。
区别:
常见的不可重入函数:
进程是由PCB、程序段和相关数据段构成的;
PCB
进程描述信息
进程控制和管理信息
资源分配清单
有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的I/O设备信息;
CPU相关信息
CPU中各个寄存器的值,当进程被切换时,CPU的状态信息都会被保存在相应的PCB中,以便进程重新执行时,能从断点处继续执行;
PCB组织方式
CPU上下文
CPU上下文切换
进程上下文
在不同时刻进程之间需要切换,让不同的进程可以在CPU执行,这个进程切换到另一个进程运行,称为进程的上下文切换;
进程是由内核管理和调度的,所以进程的切换只能发生在内核态;
进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源;
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行;
进程上下文切换的场景
状态
运行:进程正在CPU上运行;
就绪:进程处于准备运行的状态;获得了除CPU以外的所有资源;
阻塞: 进程正在等待某一时间而暂停运行;
创建:进程正在被创建,尚未转到就绪状态;
结束:进程正从系统中消失,退出运行;
状态间转换
就绪 =====> 运行:处于就绪状态的进程被调度后,获得CPU,开始运行;
运行 =====> 就绪:处于运行状态的进程时间片用完,必须让出CPU;
运行 =====> 阻塞:进程请求某一资源的使用和分配或等待某一时间的发生时,从运行状态转换成阻塞状态;
阻塞 =====> 就绪:进程等待的事件到来或请求的资源得到响应;
挂起状态
僵尸进程,子进程先于父进程结束,但是父进程没有调用 wait 或者 waitpid 回收子进程的 PCB,那么子进程成为僵尸进程,大量的僵尸进程对系统是有害的,它会一直占用系统的资源。
孤儿进程,父进程先于子进程结束,子进程成为孤儿进程,由 init 进程接管,由 init 进程负责子进程的回收,目的就是为了释放内核资源,因为进程结束后,它会释放用户空间所占用的资源,但是不会释放 PCB,即内核资源,内核资源的释放必须要由该进程的父进程来进行释放。
回收僵尸进程的资源暴力做法是把它的父进程 kill 掉,那么其子进程称为孤儿进程,init 接管,其资源也会被释放,但不是可取的行为。
正确的做法是父进程调用 wait()、waitpid() 方法回收子进程的资源。wait 函数会阻塞直到某个子进程终止,waitpid 提供了非阻塞的选项,并且可以等待指定的进程结束(通过 pid)。
管道是一种比较原始的进程间通信机制,它是一种以字节流的形式在多进程之间流动,分为无名管道和有名管道,属于半双工通信,管道是存储在内存中的,在管道创建时,内核会给缓冲区分配一个页面的大小。有名管道会存储在磁盘介质上,存在于文件系统中(p),无名管道用于具有亲缘关系的进程之间通信,有名管道可用于任意两个进程之间的通信,有名管道产生的 FIFO 文件,文件结点存储在磁盘上,但是数据还是存放在内存中的。管道与文件的区别在于管道中的数据被读取之后,管道中就没有数据了。管道是 Linux 内核所管理的固定大小的缓冲区,一般是一页的大小,4KB,管道写满了之后,调用 write 会阻塞,管道被读空了之后,调用 read 会阻塞。对于写端关闭管道进行读操作,那么 read 会返回 0;对于读端关闭的管道进行写操作时,会触发 SIGPIPE 信号,导致进程终止。
管道为什么是半双工的? ????
Linux 管道的实现是一个环形缓冲区,每个缓冲区中都有 offset 和 len 标识写入的位置,在读写的时候是要修改这些信息的,如果两个进程同时读或者同时写,那么势必会导致读写冲突,因此内核会对管道上锁,因此管道只能是半双工的。
信号量机制一般用在进程或线程之间的同步与互斥。信号量本质上一个计数器,用于控制多进程之间对共享数据对象的读取,它和管道有本质的不同,信号量机制不是以传送数据,而是以保护共享资源或保证进程同步为目标的。不存储进程间的通信数据。信号量有二值信号量和计数信号量,二值信号量相当于简单的互斥锁。
消息队列是消息的链接表,用户可以从消息队列的末尾添加消息,也可以在消息队列的头部读取消息,消息一旦被接收,就会从消息队列中移除,类似于 FIFO,但是它可以实现消息的随机查询,比如按类型字段取消息。
共享内存允许两个或多个进程共享同一块存储区,通过地址映射将该块物理内存映射到不同进程的地址空间中,那么一个进程对于共享内存中数据的修改,另一进程可以实时的看到,从而实现的进程间的通信机制,但是在多线程环境下,需要信号量来控制对共享内存的互斥访问。共享内存是最快的 IPC 机制,只需要两次数据拷贝,而管道和消息队列需要四次数据拷贝:由于管道和消息队列都是内核对象,所执行的操作也都是系统调用,而这些数据最终是要在内存中存储的,因此四次拷贝分别是
而共享内存使用 mmap 或者 shmget 函数,在内存中开辟了一块空间,映射到不同进程的虚拟地址空间中,并且向用户返回指向该块内存的指针,因此对该内存可通过指针直接访问,只需要两次:
mmap() // 建立共享内存的映射
munmap() // 解除共享内存的映射
shmget() // 获取共享内存的标识符ID
shmat() // 建立映射的共享内存
shmdt() // 解除映射
ipcs 是 Linux 用来查看进程间通信资源的工具。
ipcs -m // 查看系统使用的IPC共享内存资源
ipcs -q // 查看系统使用的IPC消息队列资源
ipcs -s // 查看系统使用的IPC信号量资源
区别
线程为什么能减少开销?
在 Linux 下,只要子进程在运行过程中,不对父进程的数据做修改,那么子进程基本是拷贝父进程的整个页表的,共享父进程的代码段、数据段、堆栈段等,即它们的物理地址是完全相同的(但注意父子进程都拥有自己的虚拟地址空间),先把页表的映射关系建立起来,并不真正进行内存拷贝,如果父子进程修改数据,那么根据写时拷贝机制,操作系统将为子进程相应的数据段和堆栈段分配物理空间,但是代码段还是继续复制父进程的地址空间(物理地址也相同)。另外子进程拥有自己的 PID、PPID 等,另外对于锁,子进程是不继承的。
对于线程来讲,同一个进程中的线程共享进程的地址空间,即线程共享进程拥有的所有资源。但它们有各自的私有栈,线程局部存储 TLS,线程切换是 TCB 切换,里边存储线程的状态、寄存器集等等。多个线程运行在单一进程的上下文中,共享这个进程虚拟地址空间的整个内容,包括它的代码、数据、数据、堆、共享库和打开的文件。
死锁是多个进程由于竞争资源而形成的一种僵局,即互相等待的问题,若无外力作用,这些进程都将无法继续执行。
临界资源是一次只允许一个进程或线程所访问的代码段。 临界区是访问临界资源的那段程序。
互斥指的是对于共享资源保证同一时刻只有一个进程或线程访问,具有唯一性和排他性,但是无法保证对资源访问的有序性。 同步主要指的是对资源的有序访问,多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务,同步基本是要求互斥的,建立在互斥的基础上。
生产者消费者模型是一个典型的同步和互斥的范例,首先生产者和消费者对于缓冲区的访问是互斥关系,同时生产者和消费者都是相互协作的关系。生产者往缓冲区中写入数据必须要保证存在非满缓冲区,消费者从缓存区中读取数据必须要保证存在非空缓冲区。
sem mutex = 1; // 临界区互斥信号量
sem full = 0; // 缓冲区初始化为空
sem empty = n; // 空闲缓冲区
//生产者:
while(1)
{
p(empty) // 获取空缓冲区资源
p(mutex) // 进入临界区
write data
v(mutex) // 离开临界区,释放互斥信号量
v(full) // 满缓冲区数加1
}
//消费者
while(1)
{
p(full) // 获取满缓冲区单元
p(mutex) // 进入临界区
read data
v(mutex) // 离开临界区,释放互斥信号量
v(empty) // 空缓冲区数加1
}
允许多个读者进行读操作,只允许一个写者进行写操作,任一写者在完成写操作之前不允许其他读者和写者对文件进行操作,写者执行写操作之前,应让已有的读者和写者退出。
对于该问题,我们应该首先使用一个计数器 count 来记录读者的数量,需要一个互斥信号量来保护对 count 的修改,还需要一个互斥信号量来保证读者写者互斥地访问文件。
int count = 0;
sem mutex = 1;
sem rw = 1;
writer{
p(rw)
write
v(rw)
}
reader{
p(mutex)
if(count == 0)
p(rw) // 第一个读进程进来阻止写进程写
++count
v(mutex)
read
p(mutex)
--count
if(count == 0)
v(rw) // 最后一个读进程读取结束释放信号量
v(mutex)
}
/* 上述实现有可能导致写进程饥饿现象发生,我们可以添加一个信号量实现写进程优先 */
/* 下述程序主要可以保证在有写请求发生时,之后的读请求都被阻塞 */
int count = 0;
sem mutex = 1;
sem rw = 1;
sem w = 1; // 用于实现写进程优先
writer{
p(w) // 在无写进程请求时进入
p(rw)
write
v(rw)
v(w)
}
reader{
p(w) // 在无写进程请求时进入
p(mutex)
if(count == 0)
p(rw) // 第一个读进程进来阻止写进程写
++count
v(mutex)
v(w)
read
p(mutex)
--count
if(count == 0)
v(rw) // 最后一个读进程读取结束释放信号量
v(mutex)
}
sem chopstick[5] = {1,1,1,1,1};
sem mutex = 1;
eat()
{
do{
p(mutex)
p(chopstick[i])
p(chopstick[(i +1) % 5])
v(mutex)
eating
v(chopstick[i])
v(chopstick[(i + 1) % 5])
think
}while(1);
}
银行家算法是著名的死锁避免算法,操作系统按照一定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,若当前系统资源可以满足其最大需求量,那么按照当前的申请量来进行分配资源,否则就推迟分配。当进程在执行中再次申请资源时,要测试该进程对资源的申请量与已分配数量之和是否超过了最大需求量。若超过那么拒绝分配资源,若为超过那么再次测试系统的现存资源是否满足该进程所需的最大资源量,若满足则分配,否则也要推迟分配。
执行安全性算法,若能找到一个安全序列,那么系统安全,否则,系统将处于不安全状态。
数据结构:可利用资源向量 available、已分配资源向量 allocation、需求矩阵 Need、最大需求矩阵 Max,Need = MAX - allocation。
原文:https://www.cnblogs.com/Trevo/p/13363297.html