文章目录
1. 进程和线程
1.1 定义
-
进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发
-
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
1.2 区别
- 一个线程只能属于一个进程,一个进程可以有多个线程,但至少有一个线程。线程依赖于进程存在
- 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。每个线程都拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。
- 系统开销:由于在创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等。因此,操作系统所付出的开销将显著的大于在创建或撤销线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只需保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。
- 通信:同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC等,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥的手段的辅助,来保证数据的一致性。有的系统中,线程的切换「同步和通信都无虚操作系统内核的干预。
- 进程编程调试简单可靠性高,但是创建销毁开销大;线程相反,开销小,切换速度快,但是编程调试相对复杂。
- 进程间不会互相影响;线程一个挂掉则整个进程挂掉
- 进程适用于多核、多机分布;线程适用于多核
1.2 进程间通信:
1.2.1管道
主要包括无名管道和命名管道:管道可用于具有亲缘关系的父子进程间的通信,有名管道还允许无亲缘关系进程间的通信
- 普通管道PIPE:
(1)半双工(数据只能在一个方向上流动),具有固定的读端和写端
(2)只能用于具有亲缘关系的进程间的通信(父子或兄弟进程).
(3)可以看成一种特殊的文件,对于它的读写可以使用普通的read,write等函数。但它不是普通的文件,不属于任何文件系统,只存在于内存中。 - 命名管道FIFO
(1)FIFO可以在无关的进程之间交换数据
(2)FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
1.2.2 系统IPC
- 消息队列
消息的链接表,存放在内核中。一个消息队列有一个标识符(即队列ID)来标记。(消息队列克服类信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限的进程可以按照一定的规则向消息队列中添加新信息;对消息队列有读权限的进程则可以从消息队列中读取信息。
特点:
1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2)消息队列独立于发送与接受进程。进程终止时,消息队列及其内容并不会被删除。
3)消息队列可以实现消息的随机查询。消息不一定要以先进先出的次序读取,也可以按消息的类型读取。 - 信号量
信号量(semaphore)与已经介绍过的IPC结构不同,它是一个计数器,可以用来控制多个进程队共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间的通信数据。
特点:
1)信号类用于进程间的同步,若要在进程间传递数据需要结合共享内存
2)信号量基于操作系统的PV操作,程序对信号量都是原子操作。
3)每次对信号量的PV操作不仅限于对信号量值加1或减1,而且可以加减任意正整数。
4)支持信号量组。 - 信号signal
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 - 共享内存(shared memory)
使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁或信号量等
特点:
1)共享内存是最快的一种IPC,因为多个进程可以同时操作,需要进行同步。
2)因为多个进程可以同时操作,所以需要进行同步
3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
1.2.3 套接字socket
可用于不同主机间的进程通信
1.3 线程的通信方式
- 临界区:通过多线程的串行化来访问公共资源或一段代码,速度快有适合控制数据访问
- 互斥量Synchronized/Lock: 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
- 信号量:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目
- 事件(信号)wait/notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作
1.4 线程上下文切换
线程在上下文切换时需要保存当前线程id、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括SP PC EAX等寄存器,其主要功能如下:
SP:堆栈指针,指向当前栈的栈顶指针
PC:程序计数器,存储下一条将要执行的指令
EAX:累加寄存器,用于加法乘法的缺省寄存器
2. 操作系统中程序的内存结构
一个程序被加载到内存中,这块内存首先就存在两种属性:静态分配内存和动态分配内存。
静态分配内存:是在程序编译和链接时就确定好的内存。
动态分配内存:是在程序加载、调入、执行的时候分配/回收的内存。
2.1 静态分配内存
-
.text: 也称为代码段(Code),用来存放程序执行代码,同时也可能会包含一些常量(如一些字符串常量等)。该段内存为静态分配,只读(某些架构可能允许修改)。
这块内存是共享的,当有多个相同进程(Process)存在时,共用同一个text段。 -
.data: 也有的地方叫GVAR(global value)数据段,用来存放程序中已经初始化的非零全局变量。静态分配。
data又可分为读写(RW)区域和只读(RO)区域。
-> RO段保存常量所以也被称为.constdata eg const数据
-> RW段则是普通非常全局变量,静态变量就在其中 -
.bss: 存放程序中未初始化的和零值全局变量。静态分配,在程序开始时通常会被清零。
其中.bss和.data合称为数据段
text和data段都在可执行文件中,由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。
2.2 动态内存分配
堆和栈都是动态分配内存,两者空间大小都是可变的。
-
Stack: 栈,存放Automatic Variables,按内存地址由高到低方向生长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大。保存程序中的局部变量(也就是在代码块中的变量)这样的变量伴随着函数的调用和终止,在内存中也相应的增加或者减少。这样的变量在创建时期按顺序加入,在消亡的时候按相反的顺序移除。
-
Heap: 堆,自由申请的空间,按内存地址由低到高方向生长,其大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大。 动态分配的内存在调用malloc()或者相关函数产生,在调用free()时释放,由程序员而不是一系列固定的规则内存持续时间,因此内存块可在一个函数中创建,在另一个函数中释放。由于这点,动态内存分配所使用的部分可能产生碎片,也就是说:在活动的内存块之间散布着未使用的内存片。动态分配内存往往要比栈分配的内存慢。
每个线程都会有自己的栈,但是堆空间是共用的。内存分为系统内存与用户内存,用户内存又分了四个部分。
其中系统内存:主要运行操作系统 ,用户内存如图所示与全文讲解。
2.3 缺页中断
malloc()和mmap()等内存分配函数,在分配时致使建立类进程虚拟内存空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。
缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
- 保护cpu现场
- 分析中断原因
- 转入缺页中断处理程序进行处理
- 恢复CPU现场,继续执行
但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:
- 在指令执行期间产生和处理缺页中断信号
- 一条指令在执行期间,可能产生多次缺页中断
- 缺页中断返回时,执行产生中断的一条指令,而一般的中断返回是执行下一条指令。
2.4 缺页置换算法
当访问一个内存中不存在的页,并且内存已满,则需要从内存中调出一个页或将数据送至磁盘对换区,替换一个页,这种现象叫做缺页置换。当前操作系统最常用的缺页置换算法如下:
- 先进先出(FIFO)算法:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后次序排成队列,从队尾进入,从队首删除。
- 最近最少使用(LRU)算法:置换最近一段时间以来最长时间未访问的页面。根据程序局部性原理,刚被访问的页面,可呢马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。
3. 并发和并行
并发(concurrency):宏观上看起来两个程序在同时运行,比如单核cpu上的多任务。但是从微观上看两个程序的指令是交织运行的,不能提高计算机的性能,只能提高效率。
并行(parallelism)指严格物理意义上的同时运行,比如多核cpu,两个程序分别运行在两个核上,两者之间互补影响,单个周期内每个程序都运行了自己的指令。
4. linux虚拟内存
为了防止不同进程在同一时刻在物理内存中运行时对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,看到的是自己独占了当前系统的4G内存。所有进程共享物理内存的,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。
进程加载创建时,内核只为进程“创建”了虚拟内存的布局,具体为初始化进程控制表中内存相关的链表,不会立即将数据和代码拷贝到对应的物理内存中,只有当运行到对应的程序时,才会引发缺页异常,开始拷贝操作。(malloc也是如此,只分配了虚拟内存,当进程真正访问到此数据时,才引发缺页异常)
虚拟内存的好处:
- 扩大地址空间
- 内存保护:进程间互不干扰,各自运行在自己的虚拟内存地址空间。虚拟内存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改
- 公平分配内存。大家都是4G
- 当进程通信时,可采用虚拟共享方式实现
- 不同进程使用相同代码时,物理内存只存储一份,不同的进程只需把自己的虚拟内存映射过去即可
- 适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。一个程序等待他的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,提高并发度
- 程序需要分配连续的内存空间时,只需在虚拟内存空间分配连续空间,不需要在实际物理内存分配连续空间,可以利用碎片
虚拟内存的代价
- 虚拟内存的管理需要很多数据结构,需要额外的空间
- 虚拟地址到物理地址的转换需要额外的指令执行时间
- 页面的换入换出需要磁盘I/O,耗时
- 一页中只有一部分数据会浪费内存
5. 线程间的同步方式
信号量
特殊变量,只能取自然数值,并且只支持两种操作:
P(SV):如果信号量大于0,将它减一;SV值为0,则挂起该线程。
V(SV):如果有其他进程因为等待SV而挂起,则唤醒,然后SV+1;否则直接SV+1
其系统调用为:
sem_wait(sem_t *sem):以原子操作方式将信号量减1,如果信号量为0,则sem_wait将被阻塞,直到这个信号量具有非0值
sem_post(sem_t *sem); 以原子操作将信号量值加1。当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒
互斥量
互斥锁,主要用于线程互斥,不能按序访问,可以和条件锁一起实现同步。当进入临界区时,需要获得互斥锁并加锁,离开时需要解锁,以唤醒其他等待该互斥锁的线程。
系统调用
pthread_mutex_init:初始化
pthread_mutex_destroy:销毁
pthread_mutex_lock::加锁(如果目标已经上锁,调用将阻塞,直到该互斥锁的占有者将其解锁)
pthread_mutex_unlock:解锁
注意:加锁解锁均为原子操作
条件变量
条件锁,用于线程之间同步共享数据的值。
线程间通信机制:当某个共享数据达到某个值时,唤醒等待这个共享数据的一个/多个线程。即,当某个共享变量等于某个值时,调用signal/broadcast。此时操作共享变量时需要加锁。
系统调用:
pthread_cond_init:初始化
pthread_cond_destroy:销毁
pthread_cond_signal:唤醒一个等待的线程(根据调度策略和优先级)
pthread_cond_wait等待目标条件变量。(需要用一个加锁的互斥锁确保操作的原子性。该函数中在进入wait状态前首先进行解锁,然后接收到信号会再加锁,保证该线程对共享资源正确访问。
6. linux的4钟锁机制
- 互斥锁:mutex,保证在任何时刻都只有一个线程访问该对象。线程获取锁失败会挂起,等待锁释放
- 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作,但同一时刻只有一个线程获得写锁。
注意:写锁会阻塞其他读写锁。 - 自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
- RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。
使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。
参考原文https://blog.csdn.net/mrcehn/article/details/79615667